runtime
235 / 250
lesson 235 Secure Python · advanced

Keep file paths inside their folder

When a file name comes from outside, `..` and absolute paths can point anywhere on the machine. Resolve the full path and refuse it unless it is still inside the folder you meant.

You'll be able to
  • Show how .. and absolute names escape a base folder when joined with /
  • Resolve the joined path and check is_relative_to(base) (not a text prefix) before opening it
  • Check every member of an archive before writing any of them (zip slip)
The idea

Programs often take a file name from a request: *download invoice-0412.pdf*, *save this upload as march.csv*. Joining that name to a folder looks safe: Path("uploads") / name. But a name is a path, and paths can climb. ../config/settings.json goes up one level and into another folder; reports/../../secret.txt does the same in disguise; and joining an absolute path like /etc/passwd throws the base away completely. This is path traversal.

The defence: build the path, resolve it (resolve() turns it into an absolute path with every .. worked out and symbolic links followed), and accept it only if it is_relative_to the resolved base folder. Comparing the actual resolved location is what counts, not what the text looks like.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Which names stay inside the folder?
from pathlib import Path

base = Path("uploads").resolve()
names = ["invoice-0412.pdf", "2025/march.csv", "../config/settings.json", "reports/../../secret.txt", "/etc/passwd"]
for name in names:
    target = (base / name).resolve()
    print(f"{name:28} inside: {target.is_relative_to(base)}")

print(Path("uploads") / "/etc/passwd")

A safe opener raises instead of returning a path outside the base, so no code path can accidentally open it:

example-2.py · A download request that tries to climb out
from pathlib import Path

def safe_path(base, name):
    root = Path(base).resolve()
    target = (root / name).resolve()
    if not target.is_relative_to(root):
        raise ValueError(f"{name!r} is outside {base}")
    return target

print(safe_path("invoices", "../app/settings.py"))

Why not check the text, for example refuse names containing ..? Because text checks miss things (an absolute path has no ..; ....// tricks; encoded forms that are decoded later) and wrongly refuse harmless names (notes..txt). And why not str(target).startswith(str(base))? Because /srv/uploads_old/x starts with /srv/uploads. is_relative_to compares whole path components.

example-3.py · Serve a stored report by name
from pathlib import Path

def read_report(base, name):
    root = Path(base).resolve()
    target = (root / name).resolve()
    if not target.is_relative_to(root) or not target.is_file():
        return None
    return target.read_text(encoding="utf-8")

Path("reports").mkdir(exist_ok=True)
Path("reports/march.csv").write_text("order_id,total\nO100282,35.84\n", encoding="utf-8")
Path("secret.env").write_text("PAYMENT_API_KEY=sk_live_51HxQe2Kd9f2a\n", encoding="utf-8")

print(read_report("reports", "march.csv"), end="")
print(read_report("reports", "../secret.env"))
print(read_report("reports", "april.csv"))

When to use it: whenever any part of a path comes from outside: downloads, uploads, report names, archive members, template names. Mistakes to avoid: joining without resolving; checking the text for .. instead of the resolved location; startswith on path strings; and checking archive members one by one *while* extracting, so the first good files are already written when a bad one is found.

Review
  • A file name from outside is a path: .. climbs and an absolute name replaces the base.
  • Resolve the joined path and accept it only if it is_relative_to the resolved base; never trust text checks or startswith.
  • For archives, check every member before writing anything.
  • Next module: once code is safe, you need to see what it does in production. The Observability & Operations module: observability.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Keep file paths inside their folder — Secure Python · Runtime