A file-download feature should return only files it is meant to expose. Path traversal happens when a supplied path can escape that intended directory. A path is a sequence of names separated here by /. The segment .. means the parent directory; . means the current directory.
Normalization means reducing a path according to the rules being used. This lesson uses a lexical model: process the printed segments without touching disk. Real file access also depends on the operating system, links and decoding rules, so this model predicts a path label rather than proving which file a real service opens.
Before you begin
- Recommended first
- Notes: scope, command, result, time, HTTP methods, headers, status codes, cookies, How a web app is put together, Same-origin policy, Authentication failures
- Estimated time
- 16 min reading, 12 min activity. Setup and optional practice may take longer.
- Where you work
- Read the original fictional requests, source fragments and state tables in a current desktop or mobile browser. The required activity needs no server, database, account or installed security tool. Do not send the printed requests to any service.
- Without a lab
- Use the complete printed evidence to trace the data or state on paper. Write the decision, the fact supporting it and one limit of the model, then compare with the activity rubric. This does not establish live service behavior. Reading works without JavaScript. Checking requires JavaScript and a secure browser context; saving also requires usable browser storage.
After this lesson you can:
- Collapse the worked pair /lab/public plus ../draft/note to the directory draft.
- Collapse the activity pair under images and name its directory.
- Explain why a server should choose a file by an id it owns.
01Example
Reduce a fictional path by its segments. This predicts a label only; neither the Python walker nor a filesystem lookup is required.
Base: /lab/public
Relative path: ../draft/note
After ..: /lab
After draft/note: /lab/draft/note
Containing directory: draft02Segments, not a prefix
Start with the base /lab/public. Appending ../draft/note adds a parent step, then draft, then note. The parent step removes public, so the result is /lab/draft/note. The final file is note and its containing directory is draft. Apply the same ordered steps to the different activity pair.
The trust boundary is between the caller's selection and the server's permitted files. Prefer mapping an allowed file identifier to a server-controlled path. If paths are required, use appropriate canonicalization and containment checks for the real filesystem, including links. A matching suffix or the letters public at the start cannot establish directory containment.
Collapse a joined path by segments, and see a parent segment leave the directory the application intended.Model. A teaching model drawn by hand. It shows the idea, not real tool output.
Read the full explanation below.
Starting location
/lab/publicResolve each path segment
/lab/publicVisible base; not a containment guarantee
Begin at /lab/public with the original relative path ../draft/note.
Follow the explanation
- Begin at /lab/public with the original relative path ../draft/note.
- The .. segment removes public.
- draft and note resolve to /lab/draft/note, inside draft.
Read every state and value
1. Begin at /lab/public with the original relative path ../draft/note.
- Action
- ../draft/note
- Stack
- lab · public
- Consumed Count
- 0
- Current Path
- /lab/public
- Boundary Label
- Visible base; not a containment guarantee
2. The .. segment removes public.
- Action
- ../draft/note
- Stack
- lab
- Consumed Count
- 1
- Current Path
- /lab
- Boundary Label
- Parent segment leaves public
3. draft and note resolve to /lab/draft/note, inside draft.
- Action
- ../draft/note
- Stack
- lab · draft · note
- Consumed Count
- 3
- Current Path
- /lab/draft/note
- Boundary Label
- Containing directory: draft
03Collapse the activity pair
Read the evidence before comparing your reasoning with the activity rubric. Do not run the script and do not open the paths.
Base and relative pairs. Resolve .. by removing the previous segment.
Do not read these paths from a disk. They are labels.
worked
base: /lab/public
relative: ../draft/note
segments after join: lab, public, .., draft, note
after .. : lab, draft, note
directory of the final segment: draft
activity
base: /lab/public/images
relative: ../../draft/seal
segments after join: lab, public, images, .., .., draft, seal
after the two .. segments: lab, draft, seal
directory of the final segment: draft
"""Lexical segment walk for the printed pairs. Does not touch a filesystem.
pathlib.Path.resolve can follow a real disk and symbolic links.
This file does not call it. Authoring did not run this file.
"""
def collapse(base, relative):
parts = []
for segment in (base.strip("/") + "/" + relative.strip("/")).split("/"):
if segment in ("", "."):
continue
if segment == "..":
if parts:
parts.pop()
continue
parts.append(segment)
return parts
def main():
worked = collapse("/lab/public", "../draft/note")
activity = collapse("/lab/public/images", "../../draft/seal")
print("worked", worked)
print("activity", activity)
if __name__ == "__main__":
main()
lab
public
images
badge.txt
private
note
draft
seal
The public directory is the intended shelf.
A joined path that still contains .. has not been placed yet.
A prefix test on the text "public" can be true of a sibling name such as public-backup.
This diagram has no public-backup directory. The walk uses segments, not a prefix.
- Write the activity base and the relative piece.
- Apply each .. by removing the previous segment. Skip nothing else on this pair.
- Check: the remaining segments are lab, draft, seal. The directory of the final segment is draft. The worked file is note, in the same directory name, by a shorter join.
- Stop. A collapsed label is not permission to open it.
04Optional challenge
Explain why allowing only names that end in .txt still fails to keep a join inside public. The rubric is: the suffix is not a segment boundary. About ten minutes.
05Fresh cases for the checks
Use these fictional cases for the checks below. They have different inputs from the worked examples. Read them here or make notes; no installation or external service is needed.
Path case A: fresh lexical walk onlyOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.
Base: /course/public
Relative: ../drafts/pad
Rule: .. removes the preceding directory segment; . changes nothing.
No symlinks or disk lookups are modeled. Do not open any path.
Path case B: separate candidate walksOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.
Intended root: /course/public
Start for each row: /course/public/pictures
P: ../badge.txt
Q: ../../private/ledger.txt
R: ../../public-backup/card.txt
Containment means under the exact public directory, not a sibling beginning with the same letters.
06Explain your evidence
Optional written self-check: explain one decision using the supplied case. No tool run is required.
Check your explanation: show each parent step; compare exact directory segments for containment; keep lexical models separate from real canonicalization and links; explain how an allowed ID keeps paths server controlled.
07Common mistakes
If the activity directory is images, you stopped before the parent segments. The relative piece has two of them.
If you call Path.resolve, you left the lexical card. The script does not call it, because resolve can follow the disk.
If a name begins with the letters public, that prefix is not the segment walk.
References
- PortSwigger: Path traversal
Explains the failure mechanism and prevention. Use it for further reading after reasoning from this page's original model; a topic page is not evidence that a lab was launched.
- Python 3.12: pathlib
The Path.resolve documentation explains absolute resolution, symbolic links and elimination of parent segments. The supplied lexical walker deliberately avoids filesystem access; a lexical prediction does not prove containment on a real disk.