Skip to content

Commit e05ac61

Browse files
committed
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
1 parent 91232d8 commit e05ac61

File tree

3 files changed

+52
-3
lines changed

3 files changed

+52
-3
lines changed

doc/build/unreleased/434.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: 434
4+
5+
Fixed issue in :class:`.TemplateLookup` where a URI with a double-slash
6+
prefix (e.g. ``//../../``) could bypass the directory traversal check in
7+
:class:`.Template`, allowing reads of arbitrary files outside of the
8+
template directory. The issue was caused by an inconsistency in how leading
9+
slashes were stripped between :meth:`.TemplateLookup.get_template` and
10+
:class:`.Template` initialization.

mako/template.py

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

262-
u_norm = self.uri
263-
if u_norm.startswith("/"):
264-
u_norm = u_norm[1:]
262+
u_norm = self.uri.lstrip("/")
265263
u_norm = os.path.normpath(u_norm)
266264
if u_norm.startswith(".."):
267265
raise exceptions.TemplateLookupException(

test/test_lookup.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,47 @@ def test_dont_accept_relative_outside_of_root(self):
127127
# this is OK since the .. cancels out
128128
runtime._lookup_template(ctx, "foo/../index.html", index.uri)
129129

130+
def test_dont_accept_relative_outside_of_root_via_double_slash(self):
131+
"""test that double-slash URI prefix can't bypass the
132+
path traversal check"""
133+
with tempfile.TemporaryDirectory() as base:
134+
tmpl_dir = os.path.join(base, "app", "templates")
135+
os.makedirs(tmpl_dir)
136+
with open(os.path.join(tmpl_dir, "index.html"), "w") as f:
137+
f.write("Hello")
138+
139+
secret = os.path.join(base, "secrets", "creds.txt")
140+
os.makedirs(os.path.dirname(secret))
141+
with open(secret, "w") as f:
142+
f.write("SECRET_KEY=supersecret123")
143+
144+
tl = lookup.TemplateLookup(directories=[tmpl_dir])
145+
rel = os.path.relpath(secret, tmpl_dir)
146+
147+
# single-slash prefix should also be blocked
148+
assert_raises_message(
149+
exceptions.TemplateLookupException,
150+
"cannot be relative outside of the root path",
151+
tl.get_template,
152+
"/" + rel,
153+
)
154+
155+
# double-slash prefix must not bypass the check
156+
assert_raises_message(
157+
exceptions.TemplateLookupException,
158+
"cannot be relative outside of the root path",
159+
tl.get_template,
160+
"//" + rel,
161+
)
162+
163+
# triple-slash prefix must not bypass the check
164+
assert_raises_message(
165+
exceptions.TemplateLookupException,
166+
"cannot be relative outside of the root path",
167+
tl.get_template,
168+
"///" + rel,
169+
)
170+
130171
def test_checking_against_bad_filetype(self):
131172
with tempfile.TemporaryDirectory() as tempdir:
132173
tl = lookup.TemplateLookup(directories=[tempdir])

0 commit comments

Comments
 (0)