CVE-2026-102991
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.
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
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 Medium
CVSS: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 Medium
CVSS: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 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…
- Record
GHSA-5639-2j2p-m4mx- CVSS 3.1
- 6.5 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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 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…
- Record
GHSA-5639-2j2p-m4mx- CVSS 3.1
- 6.5 Medium
CVSS: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 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…
- Record
PYSEC-2026-4243- CVSS 3.1
- 6.5 Medium
CVSS: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 Medium
CVSS: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
| Package | Affected | Fixed |
|---|---|---|
makoPyPI |
through 1.4.1 | 1.4.2 |
python-makoAzure Linux:3 |
through 1.2.4-3 | None known |
makoPyPI |
before 1.4.2, plus listed versions 0.1.00.1.10.1.100.1.20.1.3 Show 45 moreShow less0.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 |
makoUbuntu: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 |
makoUbuntu: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 |
makoUbuntu: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 |
makoUbuntu: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 |
makoUbuntu: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 |
makoUbuntu:26.10 |
All versions, plus listed versions 1.3.10-3build11.3.10-3ubuntu0.1 |
None known |
makoUbuntu: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
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE 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
github.com/sqlalchemy/mako/security/advisories/GHSA-5639-2j2p-m4mx
github.com/sqlalchemy/mako/issues/441
github.com/sqlalchemy/mako/commit/000ed85e4e48771eff460bf4fc721fb43de80e08
github.com/sqlalchemy/mako/releases/tag/rel_1_4_2
nvd.nist.gov/vuln/detail/CVE-2026-102991
github.com/advisories/GHSA-5639-2j2p-m4mx
Left out here: 31 versions and the rest of 3 source descriptions. The JSON record has them.