Description
AdonisJS is a TypeScript-first web framework. A Path Traversal vulnerability in AdonisJS multipart file handling may allow a remote attacker to write arbitrary files to arbitrary locations on the server filesystem. This impacts @adonisjs/bodyparser through version 10.1.1 and 11.x prerelease versions prior to 11.0.0-next.6. This issue has been patched in @adonisjs/bodyparser versions 10.1.2 and 11.0.0-next.6.
Severity: Critical 9.2 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)
AdonisJS is an MVC-oriented, “batteries-included” (think Laravel) Node.js framework with strong TypeScript support for building server-side applications. It’s commonly used to create REST/JSON APIs, backend services, and server-rendered web apps.
Technical Details
Unsafe primitives in core libraries are not inherently CVEs. When validating a vulnerability, it’s important to evaluate both the documented API contract and the de facto contract established by real-world downstream usage. Because many libraries delegate enforcement to callers, the practical security boundary often extends beyond what the documentation implies.
CVE-2026-21440 originates in a transitive dependency of AdonisJS core. When a framework surfaces such primitives directly to developers, they should be opt-in, paired with safe defaults, and/or clearly documented. In this case, it’s the combination of unsafe-by-default behavior and misleading documentation that turns an unsafe primitive into a CVE.
To handle user form submissions and uploads, AdonisJS (@adonisjs/core) parses multipart/form-data via BodyParser (@adonisjs/bodyparser) and exposes uploads as MultipartFile. The issue is in MultipartFile.move(location, options).
// adonisjs/bodyparser/src/multipart/file.ts
async move(location: string, options?: { name?: string; overwrite?: boolean }): Promise<void> {
if (!this.tmpPath) {
throw new Exception('property "tmpPath" must be set on the file before moving it', {
status: 500,
code: 'E_MISSING_FILE_TMP_PATH',
})
}
options = Object.assign({ name: this.clientName, overwrite: true }, options)
const filePath = join(location, options.name!)
try {
await moveFile(this.tmpPath, filePath, { overwrite: options.overwrite! })
this.markAsMoved(options.name!, filePath)
} catch (error) {
if (error.message.includes('destination file already')) {
throw new Exception(
`"${options.name!}" already exists at "${location}". Set "overwrite = true" to overwrite it`
)
}
throw error
}
}
By default, if options.name isn’t provided, it defaults to the unsanitized client filename and builds the destination with path.join(location, name), allowing a traversal to escape the default or intended directory chosen by the developer. If options.overwrite isn’t provided, it defaults to true, allowing file overwrites.
options = Object.assign({ name: this.clientName, overwrite: true }, options)
The AdonisJS documentation presents await file.move(app.makePath('storage/uploads')) as the default way to persist uploads, and only later suggests using a random name, framed as a uniqueness measure, rather than requiring it or documenting it as a safety requirement. This is where an unsafe primitive turns into a practical, exploitable vulnerability in applications built with AdonisJS. Developers who follow the simplest signature (file.move(path)) and the primary docs example should not end up vulnerable, yet this exact pattern can enable arbitrary file write and, in some deployments, potentially lead to RCE.
// https://docs.adonisjs.com/guides/basics/file-uploads
import app from '@adonisjs/core/services/app'
const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
/**
* Moving avatar to the "storage/uploads" directory
*/
await avatar.move(app.makePath('storage/uploads'))
The value used for the unsafe default (this.clientName) originates from the client-controlled filename parameter in the multipart Content-Disposition header. It is parsed by poppinss/multiparty (a fork of pillarjs/multiparty) and propagated through @adonisjs/bodyparser without additional validation or normalization. Multipart parsers are generally agnostic, treating the provided filename as untrusted metadata and do not decide where or how files should be persisted. While some middleware libraries choose to sanitize filenames as a convenience, the vulnerability lies in the framework layer, not multiparty.
// poppinss/multiparty/index.ts
function parseFilename(headerValue) {
var m = FILENAME_PARAM_RE.exec(headerValue)
if (!m) {
m = headerValue.match(/\bfilename\*=utf-8''(.*?)($|; )/i)
if (m) {
m[1] = decodeURI(m[1]);
} else {
return;
}
}
}
...
this.partFilename = parseFilename(this.headerValue);
...
self.destStream.filename = self.partFilename;
...
var publicFile = {
fieldName: fileStream.name,
originalFilename: fileStream.filename,
...
};
...
self.emit('file', fileStream.name, publicFile);
Impact
This vulnerability impacts any AdonisJS application where the developer uses MultipartFile.move() without the second options argument that sanitizes the client-provided filename. Exploitation requires a reachable upload endpoint.
An attacker can supply a multipart upload where the client-defined HTTP Content-Disposition header filename includes path separators and/or traversal sequences. If an application persists uploads using the documented default pattern, AdonisJS will default the destination filename to the unsanitized client filename and compute the destination path using path.join(uploadDir, name). Because path.join normalizes paths, this may resolve to a location outside the intended upload directory, enabling path traversal and, with the default overwrite, potential overwrites of existing files.
If the attacker can overwrite application code, startup scripts, or configuration files that are later executed/loaded, RCE is possible. RCE is not guaranteed and depends on filesystem permissions, deployment layout, and application/runtime behavior.
Patches
This issue has been patched in @adonisjs/bodyparser versions 10.1.2 (v6) and 11.0.0-next.6 (v7).
These patches were propogated to @adonisjs/core versions 6.19.2 (v6) and 7.0.0-next.18 (v7).
Developers can mitigate CVE-2026-21440 on the affected versions by manually validating files and sanitizing filenames e.g.
const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
if (!avatar.isValid) {
return response.badRequest({
errors: avatar.errors
})
}
await avatar.move(app.makePath('storage/uploads'), {
name: `${cuid()}.${avatar.extname}`
})
Timeline
| Date | Event |
|---|---|
| 2026-01-01 | Initial Report via GitHub Private Vulnerability Report |
| 2026-01-01 | Maintainer Acknowledgement |
| 2026-01-01 | Patch Released (6.19.2, 7.0.0-next.18) |
| 2026-01-02 | GitHub Staff Assigns CVE-2026-21440 |
| 2026-01-02 | Maintainer Releases GHSA-gvq6-hvvp-h34h |
| 2026-01-02 | Published to GitHub Advisory Database |
| 2026-01-02 | Published to National Vulnerability Database |
| 2026-01-05 | Public Exploit Published by Third Party |
| 2026-01-11 | Published to websmite.com |