Course / 09 Web security / Lesson 03 of 28

Path traversal

Estimated time: 16 min reading · 12 min activity

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

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: draft

02Segments, 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.

../draft/note

Starting location

/lab/public

Resolve each path segment

lablevel 1publiclevel 2
../draft/note
Current modeled path
/lab/public

Visible base; not a containment guarantee

Begin at /lab/public with the original relative path ../draft/note.

Follow the explanation

  1. Begin at /lab/public with the original relative path ../draft/note.
  2. The .. segment removes public.
  3. 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
Segment reduction can leave the intended directory; this is a path-label prediction, not filesystem access.

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

Download the path pairs

"""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()

Download the lexical walker

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.

Download the directory diagram

  1. Write the activity base and the relative piece.
  2. Apply each .. by removing the previous segment. Skip nothing else on this pair.
  3. 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.
  4. 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.