From 074edf5d423f97313aa2993a5af42e92ba5e2057 Mon Sep 17 00:00:00 2001
From: Mike Bayer <mike_mp@zzzcomputing.com>
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
---
 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 0000000..452265a
--- /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 82c7cba..d8ebc94 100644
--- a/mako/template.py
+++ b/mako/template.py
@@ -259,9 +259,7 @@ class Template:
             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 6a797d7..2f7cdf0 100644
--- a/test/test_lookup.py
+++ b/test/test_lookup.py
@@ -127,6 +127,47 @@ class LookupTest:
         # 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])
-- 
2.53.0

