Routing files by pattern and preventing path traversal attacks are both matching problems.
Two ways to test a path against a glob-style pattern without listing directories:
p.match(pattern)matches from the right for relative patterns:Path("a/b/c.py").match("*.py")andmatch("b/*.py")are both true.**is not recursive here; it acts like*.p.full_match(pattern)(3.13) matches the whole path and supports recursive**:full_match("src/**/*.py"). This is the one to use for rules like ignore lists or routing.
Both accept case_sensitive=.
Matching is also where path-traversal bugs live. If a web app serves base / user_input, then input like ../../etc/passwd or an absolute path escapes the base. The safe recipe:
1. Join, then resolve() to remove .. and follow symlinks. 2. Check resolved.is_relative_to(base.resolve()). 3. Reject otherwise.
Checking the string for .. is not enough (absolute paths, symlinks, encoded separators). Also reject or normalise empty strings and NUL bytes, which raise ValueError in file system calls.
For archives, the same check applies to every member name before extraction; Python 3.12+ tarfile offers filter="data" (the default from 3.14) for exactly this.
Example
from pathlib import Path, PurePosixPath
p = PurePosixPath("src/pkg/util/io.py")
print(p.match("*.py"), p.match("util/*.py")) # True True
print(p.match("src/*.py")) # False
print(p.full_match("src/**/*.py")) # True
print(p.full_match("*.py")) # False
def safe_join(base: Path, user_path: str) -> Path:
base = base.resolve()
target = (base / user_path).resolve()
if not target.is_relative_to(base):
raise PermissionError(f"outside base: {user_path}")
return target
Path("public/img").mkdir(parents=True)
print(safe_join(Path("public"), "img/logo.png").name) # logo.png
for bad in ["../secret.txt", "/etc/passwd", "img/../../x"]:
try:
safe_join(Path("public"), bad)
except PermissionError as e:
print(e)Remember
matchmatches from the right;full_match(3.13) matches the whole path with recursive**- Resolve then
is_relative_to(base)to confine user paths - String checks for '..' don't stop path traversal
Watch out for
- Serving
base / request_pathwithout resolving and checking containment.