Skip to content

Commit 72e10c5

Browse files
committed
Fix path traversal via backslash URI on Windows in TemplateLookup
The URI normalization in TemplateLookup.get_template() used posixpath which treats backslash as a literal character, but os.path.isfile() on Windows treats it as a path separator. A URI such as "\..\secret.txt" could bypass the directory traversal check. Backslash characters in URIs are now normalized to forward slashes before path resolution in both get_template() and Template.__init__. Fixes: #435 Change-Id: I68badd0f4f07ec086a1c609de335d7366daac6ef
1 parent 4ae11bd commit 72e10c5

4 files changed

Lines changed: 52 additions & 2 deletions

File tree

doc/build/unreleased/435.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.. change::
2+
:tags: bug, template
3+
:tickets: 435
4+
5+
Fixed issue in :class:`.TemplateLookup` where a URI with backslash path
6+
separators (e.g. ``\..\secret.txt``) could bypass the directory traversal
7+
check on Windows, allowing reads of arbitrary files outside of the template
8+
directory. This is an incomplete fix for :cve:`2026-41205`. Backslash
9+
characters in URIs are now normalized to forward slashes before path
10+
resolution.

mako/lookup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def get_template(self, uri):
241241
else:
242242
return self._collection[uri]
243243
except KeyError as e:
244-
u = re.sub(r"^\/+", "", uri)
244+
u = re.sub(r"^\/+", "", uri.replace("\\", "/"))
245245
for dir_ in self.directories:
246246
# make sure the path seperators are posix - os.altsep is empty
247247
# on POSIX and cannot be used.

mako/template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def __init__(
259259
self.module_id = "memory:" + hex(id(self))
260260
self.uri = self.module_id
261261

262-
u_norm = self.uri.lstrip("/")
262+
u_norm = self.uri.replace("\\", "/").lstrip("/")
263263
u_norm = os.path.normpath(u_norm)
264264
if u_norm.startswith(".."):
265265
raise exceptions.TemplateLookupException(

test/test_lookup.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,46 @@ def test_dont_accept_relative_outside_of_root_via_double_slash(self):
168168
"///" + rel,
169169
)
170170

171+
def test_dont_accept_relative_outside_of_root_via_backslash(self):
172+
"""test that backslash traversal URI can't bypass the
173+
path traversal check"""
174+
with tempfile.TemporaryDirectory() as base:
175+
tmpl_dir = os.path.join(base, "app", "templates")
176+
os.makedirs(tmpl_dir)
177+
with open(os.path.join(tmpl_dir, "index.html"), "w") as f:
178+
f.write("Hello")
179+
180+
secret = os.path.join(base, "secrets", "creds.txt")
181+
os.makedirs(os.path.dirname(secret))
182+
with open(secret, "w") as f:
183+
f.write("SECRET_KEY=supersecret123")
184+
185+
tl = lookup.TemplateLookup(directories=[tmpl_dir])
186+
rel = os.path.relpath(secret, tmpl_dir).replace("/", "\\")
187+
188+
assert_raises_message(
189+
exceptions.TemplateLookupException,
190+
"cannot be relative outside of the root path",
191+
tl.get_template,
192+
rel,
193+
)
194+
195+
# with leading backslash
196+
assert_raises_message(
197+
exceptions.TemplateLookupException,
198+
"cannot be relative outside of the root path",
199+
tl.get_template,
200+
"\\" + rel,
201+
)
202+
203+
# with leading forward slash
204+
assert_raises_message(
205+
exceptions.TemplateLookupException,
206+
"cannot be relative outside of the root path",
207+
tl.get_template,
208+
"/" + rel,
209+
)
210+
171211
def test_checking_against_bad_filetype(self):
172212
with tempfile.TemporaryDirectory() as tempdir:
173213
tl = lookup.TemplateLookup(directories=[tempdir])

0 commit comments

Comments
 (0)