NewApp Monitoring is liveSign up free
AgentbeamBeta
Course outline

Learn / Level 3 / lesson 2

Path traversal

How a file name can climb out of the folder it was meant to stay in, why checking the text is not enough, and the resolve-then-compare fix. Includes two real CVEs and a safe lab.

8 min read · Yash Thakker · updated 2026-10-24

path traversal · directory traversal · CWE-22 · secure coding

A path climbing out of an allowed folder through parent-folder steps and being stopped at a boundary check

What you will learn

  • Explain how dot-dot segments move a path out of its folder
  • Recognise the join-and-open pattern in code
  • Fix it by resolving the path and comparing it with the allowed root
  • Describe how real products were affected by path traversal

Before this: Never trust input

Many applications let you ask for a file by name: a report to download, an image to show, a template to load. The program keeps those files in one folder and trusts the name it is given. Path traversal is what happens when that name can lead out of the folder.

Paths, folders and the parent step

From files, folders and permissions, a path is an address in the file tree. Two special names matter here:

  • . means "this folder"
  • .. means "the folder above this one"

So if you are in /srv/app/reports, then .. is /srv/app, and ../config/settings.txt is /srv/app/config/settings.txt. A single name can step upward as many times as it likes. The operating system does not ask whether you meant to; it follows the steps.

The vulnerable pattern

Imagine a small download function. Files live in /srv/app/reports, and the visitor passes a file name:

import os

REPORTS = "/srv/app/reports"

def read_report(name):
    path = os.path.join(REPORTS, name)
    with open(path, "rb") as f:
        return f.read()

For name = "q3.pdf" the path is /srv/app/reports/q3.pdf, which is correct. The program checked nothing about the name; it relied on the visitor to ask only for reports. If the name contains parent steps, the joined path points somewhere else, and open happily reads it. The function never decided what "inside the reports folder" meant, so it cannot enforce it.

Two paths side by side: the vulnerable join lands outside the reports folder, the fixed version resolves the path and compares it with the allowed root

This is the flaw, catalogued as CWE-22. The consequences depend on what the program can read: configuration files, source code, keys, other users' data. Because a server process often runs with broad read access, the damage is usually bigger than the feature deserved.

Why quick fixes fail

The tempting fix is to look for the bad text:

if ".." in name:
    raise ValueError("bad name")

This is a denylist, and it is fragile for reasons that have nothing to do with cleverness. Paths can be written with different separators on different systems, URLs can encode characters before they reach your code, and a symbolic link inside the folder can point anywhere. Each time you add a rule for one variation, another is waiting. The problem is that you are checking the text of the name, not where it ends up.

The fix: resolve, then compare

Turn the path into its single, final form first, and then check that this final form is inside the allowed root. This step is called .

import os

REPORTS = os.path.realpath("/srv/app/reports")

def read_report(name):
    path = os.path.realpath(os.path.join(REPORTS, name))
    if os.path.commonpath([REPORTS, path]) != REPORTS:
        raise PermissionError("outside the reports folder")
    with open(path, "rb") as f:
        return f.read()

realpath removes the . and .. steps and follows symbolic links, giving the true location. commonpath then asks what the two paths share; if the shared part is not the reports folder, the request escaped. The decision is made on the final answer, so every spelling of "go up" fails the same way.

The fixed flow: join, resolve to the true location, compare with the allowed root, then open or refuse

Two further habits make this stronger:

  • Accept less. If reports are only ever called q1.pdf to q4.pdf, validate the name against that list or a strict pattern first. Then traversal is impossible by construction.
  • Run with less. If the server process cannot read the sensitive files at all, a bug here exposes less. This is least privilege applied to the web server's account.
Why is comparing the resolved path better than searching the name for dot-dot?

The resolved path is the true destination, no matter how the name was written or whether a symbolic link redirects it. Searching the text only catches the spellings you thought of.

Spotting it in a code review

  • Find every use of a file-opening function (open, readFile, send_file, File(...)) and trace the argument backwards.
  • If any part came from a request, header, database field or archive entry, look for a resolve-and-compare step. A join alone is the red flag.
  • Check that the check happens on the resolved path, and that the file is opened after the check using the same resolved value.
  • Look at the process privileges. A web server that can read /etc or the whole home folder is making every path bug worse.

Real cases

Path traversal is not a beginner-only mistake. Two entries on CISA's Known Exploited Vulnerabilities list, meaning they have been seen attacked in the wild, show two shapes of it.

Both are rated high severity, and both can leak files without any login. The common thread is not an unusual technique but a missing boundary check.

How AI changes this

Attackers. Path traversal is easy to find by pattern, and automated scanners have tested for it for years. What AI changes is the cost of reading code: a model can scan a large repository for places where a request value reaches a file call, then adapt to the framework and custom filters it finds. The weakness being searched for is the same; the time to find it shrinks. Reports of AI-assisted exploit work on Beam's blog, such as the Hacktron write-up, show that.

Defenders. AI review is useful for this exact job: "list every function that opens a file using a request-derived name and show whether it resolves and compares." It is quick to produce a candidate list. Check each one yourself and write a regression test, because a model may say a path is safe when a symbolic link says otherwise.

AI agents. An agent with file tools is a path-handling program whose input is language. If it is told to write to notes/summary.md but a page it read convinces it to write to ../../.ssh/authorized_keys, the same bug appears through a different door. Agents also run as you, so a traversal reaches everything you can reach. The fix is the same: the tool resolves the path and refuses anything outside an allowed root, rather than hoping the model behaves. Beam's covers the agent angle, and the entry explains why a confined folder matters.

Try it

This lab stays inside a folder you create. Nothing outside it is touched, and a "secret" file is simply one you make yourself.

mkdir -p ~/traversal-lab/public && cd ~/traversal-lab
echo "this is a public report" > public/report.txt
echo "pretend secret" > secret.txt

Save this as lab.py in ~/traversal-lab:

import os

ROOT = os.path.realpath("public")

def unsafe(name):
    return open(os.path.join(ROOT, name)).read()

def safe(name):
    path = os.path.realpath(os.path.join(ROOT, name))
    if os.path.commonpath([ROOT, path]) != ROOT:
        raise PermissionError("outside the folder")
    return open(path).read()

for name in ["report.txt", "../secret.txt"]:
    for fn in (unsafe, safe):
        try:
            print(fn.__name__, name, "->", fn(name).strip())
        except PermissionError as e:
            print(fn.__name__, name, "-> refused:", e)

Run python3 lab.py. The unsafe function returns the pretend secret from one folder above public, while the safe one refuses. Now change report.txt into a symbolic link to secret.txt with ln -sf ../secret.txt public/report.txt, run it again, and notice that safe also refuses a name that looks harmless. That is why resolving beats inspecting text. Clean up with cd ~ && rm -r ~/traversal-lab.

Check yourself

What is the root cause of path traversal, in one sentence?

A program joins an outside name onto a folder and opens the result without checking that the final location is still inside that folder.

A developer rejects any name containing '..' and says the endpoint is safe. Name one way it can still be unsafe.

A symbolic link placed inside the folder can point anywhere, and the name has no dot-dot at all. Encoded or alternative separators may also slip past a text check. The check must run on the resolved path.

Why did the DrayTek record matter more because the process ran as root?

Path traversal lets a visitor read whatever the server process can read. A process with root rights can read nearly every file on the system, so the same missing check exposed far more than the intended download folder.

Next

Paths were the first place data became structure. The next lesson moves to the database, where a name typed into a login box can stop being a name and start being a command.