From e05ac61989a7fb9dd7dcde6cfd72dc48328719a3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 14 Apr 2026 15:45:19 -0400 Subject: [PATCH] Fix path traversal via double-slash URI prefix in TemplateLookup The URI normalization in Template.__init__ stripped only a single leading slash, while TemplateLookup.get_template() stripped all leading slashes. A URI such as "//../../secret.txt" could bypass the directory traversal check. Changed to use lstrip("/") so both code paths handle leading slashes consistently. Fixes: #434 Change-Id: I400b9a40aed956cc2b5826a9c8736f104e84f1a4 CVE: CVE-2026-41205 Upstream-Status: Backport [https://github.com/sqlalchemy/mako/commit/e05ac61989a7fb9dd7dcde6cfd72dc48328719a3] (cherry picked from commit e05ac61989a7fb9dd7dcde6cfd72dc48328719a3) Signed-off-by: Hetvi Thakar --- doc/build/unreleased/434.rst | 10 +++++++++ mako/template.py | 4 +--- test/test_lookup.py | 41 ++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 doc/build/unreleased/434.rst diff --git a/doc/build/unreleased/434.rst b/doc/build/unreleased/434.rst new file mode 100644 index 00000000..452265ad --- /dev/null +++ b/doc/build/unreleased/434.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, template + :tickets: 434 + + Fixed issue in :class:`.TemplateLookup` where a URI with a double-slash + prefix (e.g. ``//../../``) could bypass the directory traversal check in + :class:`.Template`, allowing reads of arbitrary files outside of the + template directory. The issue was caused by an inconsistency in how leading + slashes were stripped between :meth:`.TemplateLookup.get_template` and + :class:`.Template` initialization. diff --git a/mako/template.py b/mako/template.py index 82c7cba8..d8ebc949 100644 --- a/mako/template.py +++ b/mako/template.py @@ -259,9 +259,7 @@ def __init__( self.module_id = "memory:" + hex(id(self)) self.uri = self.module_id - u_norm = self.uri - if u_norm.startswith("/"): - u_norm = u_norm[1:] + u_norm = self.uri.lstrip("/") u_norm = os.path.normpath(u_norm) if u_norm.startswith(".."): raise exceptions.TemplateLookupException( diff --git a/test/test_lookup.py b/test/test_lookup.py index 6a797d7a..2f7cdf0b 100644 --- a/test/test_lookup.py +++ b/test/test_lookup.py @@ -127,6 +127,47 @@ def test_dont_accept_relative_outside_of_root(self): # this is OK since the .. cancels out runtime._lookup_template(ctx, "foo/../index.html", index.uri) + def test_dont_accept_relative_outside_of_root_via_double_slash(self): + """test that double-slash URI prefix can't bypass the + path traversal check""" + with tempfile.TemporaryDirectory() as base: + tmpl_dir = os.path.join(base, "app", "templates") + os.makedirs(tmpl_dir) + with open(os.path.join(tmpl_dir, "index.html"), "w") as f: + f.write("Hello") + + secret = os.path.join(base, "secrets", "creds.txt") + os.makedirs(os.path.dirname(secret)) + with open(secret, "w") as f: + f.write("SECRET_KEY=supersecret123") + + tl = lookup.TemplateLookup(directories=[tmpl_dir]) + rel = os.path.relpath(secret, tmpl_dir) + + # single-slash prefix should also be blocked + assert_raises_message( + exceptions.TemplateLookupException, + "cannot be relative outside of the root path", + tl.get_template, + "/" + rel, + ) + + # double-slash prefix must not bypass the check + assert_raises_message( + exceptions.TemplateLookupException, + "cannot be relative outside of the root path", + tl.get_template, + "//" + rel, + ) + + # triple-slash prefix must not bypass the check + assert_raises_message( + exceptions.TemplateLookupException, + "cannot be relative outside of the root path", + tl.get_template, + "///" + rel, + ) + def test_checking_against_bad_filetype(self): with tempfile.TemporaryDirectory() as tempdir: tl = lookup.TemplateLookup(directories=[tempdir])