How Path Traversal Bugs Let Attackers Read Files Outside the Web Root
Path traversal turns a filename parameter into a read of any file the server can open. Learn why filters fail, what confinement means, and why a file read can be a full compromise.
Path traversalPath Traversal🛡️A web vulnerability (CWE-22) where user-supplied input in a file path escapes the directory the application intended to serve from, typically via parent-directory references, letting an attacker read or write files elsewhere on the server. is one of the oldest bug classes on the web, and it keeps producing maximum-severity CVEs. The GitLab commits API flaw CVE-2026-85706, patched in September 2026 and exploited within a day, is a textbook case: an unauthenticated request with a crafted file path returned arbitrary files from the server. This article explains why the bug class persists, what makes one instance catastrophic and another trivial, and how to reason about it in your own systems.
What the Bug Actually Is
Almost every web application takes a user-supplied name and turns it into a filesystem path. A download endpoint takes a filename. A repository API takes a path inside the repo. A template engine takes a view name. Somewhere in the code, that string is joined to a base directory and handed to the operating system's file-open call.
Path traversal happens when the joined path escapes the base directory. The classic mechanism is the parent-directory reference: two dots and a separator. If the application concatenates a base like `/srv/app/uploads/` with user input like `../../etc/passwd`, the operating system normalizes the result to `/etc/passwd` and returns it. The application believed it was serving from its uploads folder. The kernel did what it was told.
The weakness is catalogued as CWE-22, improper limitation of a pathname to a restricted directory. That name captures the real issue: the bug is not the presence of dots in a string, it is the absence of a confinement check after the path is resolved.
Why Naive Filters Fail
Most first attempts at a fix are string filters. Strip `../`. Reject any input containing two dots. These fail for well-understood reasons.
Encoding is the first problem. The same characters can arrive URL-encoded, double-encoded, or in an overlong UTF-8 form, and a filter that runs before the framework decodes the parameter sees nothing suspicious. Stripping is the second problem: removing `../` from `....//` leaves `../` behind. Platform differences are the third: a filter written for forward slashes can miss backslashes on Windows, and case-insensitive filesystems can defeat exact-match denylists.
The durable fix is to resolve the path first and check containment second. Canonicalize the full path, including symlink resolution, and then verify that the result still starts with the intended base directory. If it does not, refuse. This is the property GitLab's advisory describes as path confinement, and it was one of the two controls missing in the commits API.
Why Authentication Matters as Much as Confinement
A traversal bug that requires an authenticated session is bad. A traversal bug reachable without credentials is a different tier of severity, because every scanner on the internet can exploitExploit🛡️Code or technique that takes advantage of a vulnerability to cause unintended behavior, such as gaining unauthorized access. it at scale. GitLab's description of CVE-2026-85706 names both failures: improper path confinement and missing authentication enforcement. The second one is what turned a code-quality defect into a CVSS 10.0 event with a three-day CISA remediation deadline.
Defense in depthDefense in Depth🛡️A security strategy using multiple layers of protection so that if one layer fails, other layers continue to provide security. is the point. If the endpoint had enforced authentication, the traversal would have been limited to registered users and would have shown up in logs with a user identity attached. If the confinement check had been correct, the authentication gap would have exposed only repository contents that were already readable. Neither control alone is sufficient, and the absence of both is what produces internet-wide exploitation.
What Gets Read Decides the Severity
A file read is only as dangerous as the files the application user can open. On a static site, traversal might expose HTML templates and little else. On a platform like GitLab, the application process can read its own configuration and secrets, and those files are the keys to everything.
GitLab's documentation identifies the secrets file on Linux package installs as `/etc/gitlab/gitlab-secrets.json` and states that it holds the database encryptionEncryption🛡️The process of converting data into a coded format that can only be read with the correct decryption key. key. With that key, encrypted database columns become readable: CI/CD variables, runner tokens, deploy tokens, integration credentials, webhook secrets. This is why watchTowr characterized the GitLab bug as a path to source code, CI/CD secrets, credentials, and pipeline injection rather than as a simple information leak. The companion piece Why One File Read Can Compromise Your Entire CI/CD Platform follows that chain to its end.
Application logs are the other high-value target. Request logs frequently contain tokens in query strings, session identifiers, and internal hostnames. Attackers who land a traversal read will often pull logs first, because logs tell them where the secrets are.
How Exploitation Looks in Practice
The GitLab case is instructive because exploitation was so simple. According to watchTowr, a single HTTP POST to the project commits endpoint with a crafted file path parameter was enough, and the only precondition was that the instance host at least one public project. There was no memory corruption, no race, no multi-stage chain. Scanners could reproduce it from the description.
This is why disclosure-to-exploitation windows for traversal bugs are short. A memory-safety bug requires an exploit developer to build a reliable primitive. A traversal bug requires someone to guess a parameter name. The site's coverage of the collapsing patch-to-exploit window has documented the same dynamic across several products this year, and traversal bugs sit at the fast end of that curve.
Detecting It
Request logs are the primary evidence. Look for parameters containing parent-directory sequences, in both raw and encoded forms, arriving at endpoints that accept file or path arguments. Successful reads typically return a 200 with a response body size that does not match the expected artifact. For the GitLab flaw, the vendor-independent detection guidance was to search for POST requests to the commits endpoint carrying a file path parameter, and GitLab's request log records method, path, params, remote IP, and status for every API call.
Web application firewalls can block obvious traversal patterns, but encoded and platform-specific variants get through, so treat a WAF as a tripwire rather than a fix.
Preventing It in Code You Own
For your own applications, the checklist is short and does not change much between languages:
- Resolve the full path before checking it, and check that the resolved path is inside the allowed base directory.
- Prefer indirect references. Map user-supplied identifiers to a server-side table of allowed files rather than accepting paths at all.
- Run the application as a user that cannot read secrets it does not need, and keep secrets out of the web root and out of any directory the application serves.
- Require authentication on every endpoint that touches the filesystem, and log the identity that made each request.
The last point deserves emphasis. If GitLab's commits endpoint had required a session, the blast radiusBlast Radius🛡️The full set of systems, data, and access an attacker can reach after compromising a given asset. Ranking assets by blast radius rather than by how exposed they are pushes high-reach systems like a firewall management console to the top of the priority list. of the traversal would have been smaller and the attribution trail longer. Restricting who can reach a vulnerable code path is a control you can apply today, before the next CVE, and How to Lock Down Public Projects on Self-Managed GitLab shows what that looks like for one widely deployed platform.