Serve an uploaded file without path traversal

A download endpoint joins the requested name onto the upload directory and opens it, so ../ or an absolute path reads files anywhere on disk, and a symlink in the directory points outside it. Repair it to resolve the final path and refuse anything outside the upload directory.

Requires Node.js 22 or newer, which runs the tests, and Python 3.12 or newer, which runs your code (the command python3, or the program named in the PYTHON environment variable). No package installation or server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit uploads.py, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same grader with your code in Pyodide (CPython compiled to WebAssembly); locally it runs your code with python3. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. Reproduce the escape
Run the starter. The name "../secret.txt" and an absolute path both read files outside the upload directory, and the symlink name reads the secret it points to.

2. Resolve the real target path
Compute base = os.path.realpath(base_dir) and target = os.path.realpath(os.path.join(base, name)). realpath collapses .. and resolves symlinks, so the check sees where the path really leads.

3. Require the target inside the directory
Use os.path.commonpath([base, target]) == base to confirm the target is within base, and raise ValueError otherwise. os.path.join already turns an absolute name into an escape, which this check catches.

4. Keep legitimate downloads
notes.txt and sub/data.txt still return their contents. Resolving the real path, rather than editing the name, is what makes the symlink and the .. cases safe.

SUPPLIED CONTRACT
read_upload(base_dir, name) returns the text contents of the file name inside base_dir, or raises ValueError when the name would read outside base_dir. The lab supplies base_dir as an absolute path, which may end with a separator, to an upload directory holding notes.txt and sub/data.txt, a secret.txt one level above it, and a symlink inside the directory that points at that secret. Resolve both base_dir and the joined target to their real absolute paths with os.path.realpath, which also follows symlinks, and require the target to be inside base using os.path.commonpath. Open and read the file only when it is inside; otherwise raise ValueError. Do not try to clean the name by stripping ".." yourself.

REFERENCE
reference.py contains an explained solution: notes.txt and sub/data.txt resolve to paths inside the upload directory and return their contents. "../secret.txt" resolves to the secret one level up, so commonpath is the parent, not base, and the function raises; an absolute path such as /etc/hostname does too, because os.path.join drops base when the second argument is absolute. The symlink inside the directory resolves through realpath to the secret outside, so it is refused even though its own name looks ordinary. A file named upload beside the uploads directory is refused too: its path is a prefix of the directory’s as text, but commonpath compares whole components. base_dir given with a trailing separator still reads notes.txt, because realpath normalizes it as well. A name for a file that does not exist raises as well, so nothing is read. Dotless, well-behaved names keep working, because the fix checks the resolved location rather than editing the name.

SCOPE
The lab uses a small in-memory filesystem the grader builds, and reads text files. A real file server also needs to avoid a time-of-check/time-of-use race between the check and the open (open the file, then verify with os.path.realpath of /proc/self/fd or O_NOFOLLOW), restrict by a filename allowlist where it can, set correct content types, and apply per-user authorization: traversal prevention is one layer of several. The archive-extraction lab applies the same idea to ZIP entries.

NEXT TASK
Where does your code build a filesystem path from a request value, and does it check the resolved real path against the directory it must stay in?

SOURCE
https://owasp.org/www-community/attacks/Path_Traversal
