Research
Researchsecurity13 min read

CVE-2026-21440 and AI file tools show path traversal is an architectural antipattern

Path traversal bugs in AdonisJS, Gogs, rsync and AI development tools point to the same design failure: modern frameworks still delegate file safety to application code at the worst possible boundary.

AdonisJS Bodyparser CVE-2026-21440, Gogs CVE-2025-8110, rsync CVE-2024-12087 and directory access bypasses in AI file tools show the same path traversal failure across AI-assisted development tools, cloud infrastructure and open-source frameworks. The bug class is old enough to be boring, but the recent pattern is not. Each case points at the same architectural weakness: modern software still treats user-controlled filenames as strings that can be made safe later.

That assumption keeps failing. It fails in multipart upload handlers where a filename is treated as display metadata until it is passed to a move operation. It fails in repository editors where a path inside a project can resolve through a symbolic link into a different part of the filesystem. It fails in synchronisation tools where a remote peer can influence where a local client writes. It fails in AI agents where the model is asked to manage files and the adapter turns that request into filesystem access with a thin validation layer between intent and authority.

The common thread is not that developers forgot to block ../. That explanation is too small. The deeper problem is that too many frameworks still make path safety a property of application code rather than of the file capability itself. Every downstream project is expected to remember the same sequence: normalise the path, reject absolute paths, collapse traversal segments, resolve symbolic links and compare canonical locations. That is not a secure abstraction. It is a memorisation test with production file writes attached.

CVE-2026-21440 shows the upload API problem

The AdonisJS Bodyparser vulnerability, tracked as CVE-2026-21440 with a reported CVSS score of 9.2, is a useful anchor because the mechanics are familiar. The research brief describes a flaw that allows arbitrary file writes when MultipartFile.move() is used without sanitising attacker-controlled filenames. A remote attacker can submit a multipart upload with a crafted filename containing traversal sequences and cause the server to write outside the intended upload directory.

There is nothing exotic in that chain. The attacker controls a filename. The framework preserves it. The application calls a convenience method. The resulting write crosses a directory boundary. What makes the bug important is that each individual step looks natural in isolation.

Multipart upload APIs often present the uploaded file as an object with a name, a temporary location and a helper for moving it into permanent storage. That interface is pleasant to use. It also carries a dangerous implication: the filename supplied by the client is treated as a usable filesystem component. In reality, a filename in a multipart request is closer to an HTTP header than to a local path. It is untrusted metadata supplied by the adversary. It may be useful for display after sanitisation. It should not be authority to choose where a server writes.

Developers reach for upload helpers precisely to avoid writing unsafe file handling code. If the safe-looking API still accepts a hostile path component and joins it to a destination directory, the framework has exported the hard part. The application author is left to know which fields are merely labels, which are path components and which helper methods preserve attacker influence. In a large codebase, one missed call site is enough.

The lesson from CVE-2026-21440 is not only that filenames should be sanitised. It is that a framework should not make the dangerous behaviour the ergonomic one. A safe upload API would treat the client filename as inert metadata and require a server-generated storage name by default. If the application wants to preserve the original name, it should have to opt into a constrained basename operation that cannot express directories. The default should be a generated object key, not a partially trusted path.

Gogs and rsync show traversal outside web uploads

Gogs CVE-2025-8110 broadens the point. The reported vulnerability affects improper symbolic link handling in the repository editor's PutContents API and carries a CVSS score of 8.7. The brief says it allows code execution and is being actively exploited in the wild. This is still path traversal, but it is not the stereotypical ../../etc/passwd upload bug. The boundary is a repository. The bypass is symbolic link handling. The impact can reach execution because writing through a repository interface can place attacker-controlled content into locations that later get interpreted as code or configuration.

Symbolic links are where naive path validation fails. A string check can prove that docs/file.txt does not contain ... It cannot prove that docs is not a link to somewhere else. Even canonicalisation is not enough if the path is checked and then opened later through a name that the attacker can swap. That is the old time-of-check-time-of-use problem that Google Project Zero discussed in James Forshaw's 2019 analysis of Windows kernel file handling. The details concerned Windows IO Manager behaviour, object manager symbolic links and CVE-2016-3219, but the underlying shape is general: the security property depends on how names are resolved by the operating system, not how they looked to the caller before resolution.

Rsync adds another version of the same failure. The 2024 vulnerability set included CVE-2024-12087, which NVD scores at CVSS 6.5 and describes in the brief as an external directory file-write vulnerability. In a malicious-server scenario, attackers influence client-side file writes, exposing sensitive material such as SSH keys or enabling execution by overwriting startup files like .bashrc. Again, the vulnerable surface is not a web form. It is a file synchronisation protocol where one side is trusted to describe file paths and the other side materialises them locally.

These cases matter because they show why path traversal should be treated as a general authority bug. A path is not just a location. It is a request to traverse a namespace with rules that include relative segments, mount points, links, case behaviour, Unicode normalisation and platform-specific device names. The moment untrusted input can influence that traversal, the program is delegating part of its authority to the attacker.

AI development tools turn paths into agent authority

The AI-assisted development ecosystem is now putting this old bug class under new load. Research tracked by embracethered describes vulnerabilities across GitHub Copilot, Claude Code, Cursor, Amazon Q Developer, Google Jules, Windsurf and OpenHands. The issues include prompt injection, data exfiltration, remote code execution, secrets exposure and file access bypasses. One item in the brief identifies the Anthropic Filesystem MCP Server with a directory access bypass caused by improper path validation.

The important change is not that AI tools have files. IDEs and build tools have always had files. The change is that AI agents routinely translate high-level intent into low-level filesystem operations. A user asks an agent to inspect a project, modify a script, load a document or run a tool. The agent calls an adapter. The adapter receives paths. Somewhere in that chain, a natural-language instruction becomes read, write, move or execute against a local or cloud filesystem.

That bridge is fragile. Prompt injection can change the agent's intent. Tool descriptions can shape what the model believes is safe. Hidden instructions can cause an agent to request files the user did not mean to expose. If the filesystem layer then performs weak containment checks, the model's mistake becomes a filesystem breach. The path traversal bug does not need to originate in the model. The model only needs to supply, select or relay a path that the underlying tool fails to constrain.

MCP makes the pressure visible because it standardises tool access. A filesystem MCP server is often given a root directory and asked to expose controlled read and write operations to an agent. That sounds narrow. It is narrow only if the server resolves paths exactly once, against the intended root, using operating-system primitives that preserve containment. If it joins strings and checks prefixes, it is a directory escape waiting for the right input. If it resolves .. but not symlinks, the boundary is porous.

AI tools also change the exposure model. A traditional file upload vulnerability needs an attacker to reach the vulnerable route. An agentic development tool may process repository content, issue tracker text, documentation or generated files as part of its workflow. That means attacker-controlled content can appear inside the agent's decision loop even when the attacker has no account on the developer's machine. If the agent can be induced to operate on a crafted path and the tool layer trusts that path, the boundary between content and capability collapses.

The architectural antipattern

The antipattern has four parts.

First, the framework accepts a user-controlled path or filename as a string. This could be a multipart filename, a repository path, an archive member name, a synchronised file entry or an MCP tool argument.

Second, the framework exposes a convenience operation that appears to preserve a boundary: move this upload into that directory, edit this file inside that repository, sync this tree into that destination, read this path under this workspace. The boundary is described in documentation or caller intent, not enforced as a primitive.

Third, the implementation attempts to recover safety through string processing. It strips prefixes, rejects .., calls a normalisation function or checks that the resulting path starts with the intended root. These measures catch simple cases and fail at the edges: symlinks, races, absolute paths, alternate separators, platform quirks and names that change meaning between validation and use.

Fourth, the dangerous operation happens through a path name rather than through a constrained capability. By the time the program calls open, rename, write or execute, the operating system is asked to resolve a name. If the attacker can influence that name or any component along the route, the original boundary is advisory.

The fix is to stop designing APIs where every caller has to repeat the same defensive ritual. A safer model gives the caller a capability to a directory and exposes operations relative to that capability. The implementation opens the base directory once, resolves children relative to that handle and refuses to follow links or escape the tree. On platforms that support it, this means using directory file descriptors and openat-style APIs with flags that constrain link following. On Windows, it means understanding object manager semantics, reparse points and the distinction between names before and after resolution. The exact primitive differs by platform. The principle does not.

String filtering can still have a role, but only as input hygiene. It is not an authority boundary. A basename function can remove directory components from a display filename. A normaliser can reduce accidental weirdness. Neither should be the thing preventing an attacker from writing to .ssh/authorized_keys or reading a parent directory.

Why frameworks keep getting this wrong

Frameworks get this wrong because secure file handling is awkward to make portable and easy to use. The simple API is move(destination, filename). The correct API is closer to store this byte stream under a server-chosen name inside this already-open directory capability, without following links, while preserving atomicity and rejecting any operation that would leave the capability. That is harder to document. It is also less flattering to the developer's sense that a path is just a string.

Cross-platform behaviour makes the problem worse. Unix, Windows and container filesystems do not resolve names in identical ways. Case sensitivity varies. Reserved names vary. Link semantics vary. Cloud object stores add another wrinkle because keys look like paths without always behaving like filesystems. Developers build abstractions over these differences, then accidentally expose the leakiest mental model to users.

There is also product pressure. Developer tools compete on speed. Upload handlers, repository editors and AI filesystem plugins are judged by how quickly they can be wired into an application. The secure design is rarely the shortest code sample. Documentation shows the ergonomic path, users copy it and the security boundary becomes a comment in the guide.

AI tools intensify this because the selling point is delegated action. The assistant can read the project, edit files and call tools on the user's behalf. Any friction in filesystem access looks like reduced capability. A strict capability model is therefore treated as a limitation until the first directory escape turns it into an incident report.

What safer file APIs should look like

A safer design starts by separating names for humans from names for storage. A client-supplied filename should be display metadata. Storage names should be generated by the server or selected from a constrained policy that cannot express hierarchy. If an application needs to preserve an extension, preserve only the extension after parsing it as data, not as a path.

The second requirement is containment by construction. APIs should accept a base directory capability and a relative child name that has already been reduced to a single path component unless hierarchy is explicitly required. If hierarchy is required, each component should be resolved under the base using primitives that prevent link traversal out of the tree. Prefix checks on absolute strings should be treated as code smell.

The third requirement is race resistance. Validation and use must not be separable when an attacker can modify the filesystem between them. Repository editors, archive extractors, synchronisation clients and AI workspace tools all need to assume that parts of the tree may be attacker-controlled. Checking a path and then opening it later through the same name is not a boundary.

The fourth requirement is capability reduction for agents. An AI tool that only needs to read src/ should not receive access to the whole project. A tool that writes generated documentation should not be able to overwrite shell startup files, editor configuration or dependency manifests unless explicitly granted. The permission should attach to a resolved directory or file class, not to the model's current interpretation of a path string.

The final requirement is hostile test coverage. Unit tests that prove ../secret is rejected are not enough. Test symlinks. Test nested symlinks. Test absolute paths. Test alternate separators. Test archive entries. Test case variants where relevant. Test the exact helper methods that documentation tells users to call.

The old bug class fits the new stack too well

Path traversal persists because it sits at the boundary between two comforting lies. The first is that strings can faithfully represent authority. The second is that a framework can expose the operating system's filesystem semantics while hiding the parts that make them dangerous. Both are convenient. Neither survives contact with an attacker.

The recent spread across AdonisJS, Gogs, rsync and AI development tooling should make defenders less tolerant of the usual advice. "Sanitise the path" is not enough. It was never enough. The more useful question is whether the caller ever receives the authority to name a location outside its intended boundary. If the answer is yes, the design is already depending on every future validation step being correct.

The filesystem has always been a namespace full of traps. The difference now is that modern frameworks keep handing the map to increasingly autonomous tools, then asking a string filter to stand guard at the border.

Newsletter

One email a week. Security research, engineering deep-dives and AI security insights - written for practitioners. No noise.