Step 1 of 5 · Reading · ~4 min
Read
Concurrency & Persistence
Serving Static Files
Most of what a web server sends is not generated, it is a file on disk. That makes the static handler the simplest thing in the server and by some distance the most dangerous, because it is the one place where a string the attacker controls becomes a path the operating system will open. Two things must be right: where the path is allowed to point, and what you claim the bytes are.
The one-line vulnerability
The first version everyone writes is: take the document root, append the request path, open the result. It works on every path you would test by hand. Then a client sends:
GET /../../../etc/passwd HTTP/1.1
root joined with it: /var/www/../../../etc/passwd
the kernel resolves: /etc/passwd
This is path traversal. It is decades old and still lands in production every year, because the broken version is the one you write first.
Canonicalise first, then check once
The fix is two steps in a fixed order:
- Turn the target into one canonical path: cut the query string, percent-decode once, join it to the root, and resolve
.and..and repeated slashes. - Ask a single question about the result: is it the root, or is it inside the root?
Checking the raw string for .. before decoding is the bypass factory. The attacker sends /%2e%2e/etc/passwd or /..%2fetc%2fpasswd, your blacklist reads a string with no .. in it, and the kernel sees one after decoding. Decode first, normalise second, check last.
Decode exactly once. /%252e%252e/x decodes to the literal text /%2e%2e/x, which is an ordinary file name. Decoding twice would turn it into a traversal after your single check had approved it. A malformed escape or a decoded NUL byte (%00, once used to truncate paths in C) is a 400.
Two details earn their place in the containment test:
- It must compare against the root plus a slash. A bare prefix test accepts
/var/www-backup/secretfor the root/var/www, and "sibling directory with the same first letters" is a real escape. - On a real disk, canonicalising by text is not enough: a symlink inside the root can point outside it. Production code resolves symlinks (
realpath) before the containment test. This exercise has no disk, so it does the textual half.
What the client asked for
A path that ends in /, or that normalises to the root itself, names a directory, and a directory is served as its index.html. So / becomes <root>/index.html and /static/ becomes <root>/static/index.html.
Content-Type: telling the truth about bytes
A browser decides what to do from Content-Type, not from the extension in the URL. Serve a stylesheet as text/plain and the page renders unstyled; serve a script as text/plain and modern browsers refuse to run it. The type comes from the last extension of the final path component, compared case-insensitively. A name with no dot, a leading-dot name such as .htaccess, and a trailing dot all have no extension.
| Extension | Content-Type |
|---|---|
.html | text/html; charset=utf-8 |
.css | text/css; charset=utf-8 |
.js | text/javascript; charset=utf-8 |
.json | application/json |
.png .jpg .jpeg .gif | image/png image/jpeg image/jpeg image/gif |
.svg | image/svg+xml |
.txt | text/plain; charset=utf-8 |
.wasm .woff2 .ico | application/wasm font/woff2 image/x-icon |
Anything else is application/octet-stream: "raw bytes, do not interpret". Defaulting to text/html would let an uploaded file full of markup run as script in your origin. Guessing is the vulnerability, refusing to guess is the fix.
The contract
Each input line is <root>|<target>, where the root is an absolute directory without a trailing slash. Print <canonical path>|<content type>, or 403 if the path is outside the root, or 400 for a target that does not start with /, has a malformed escape, or decodes to a NUL.
Common mistakes
- Joining the raw target and checking
..as a substring. - Decoding twice, or checking before decoding.
- Using a prefix test without the trailing slash.
- Forgetting that
/and/static/are directories. - Taking the extension from the first dot (
app.min.js) instead of the last.
Your exercise
The starter reads the lines and splits root from target. You canonicalise, contain, resolve directories and choose the type.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…