Feb 8, 2026·6 min read·9 visits
The AdonisJS body parser initialized form field storage as a plain object (`{}`). This allowed attackers to inject properties into `Object.prototype` by sending form fields named `__proto__.key`. This pollutes the entire application runtime, affecting every object in the process.
A critical Prototype Pollution vulnerability in the AdonisJS `@adonisjs/bodyparser` package allows unauthenticated attackers to corrupt the global object prototype via crafted multipart form-data requests. By manipulating the `__proto__` property during field accumulation, attackers can trigger Denial of Service, authentication bypasses, or potentially Remote Code Execution depending on the application's gadget chain.
Web frameworks have a tough job. They stand as the bouncer at the club door, patting down every request that tries to get in. One of the messiest parts of that job is handling multipart/form-data. It's a protocol designed in a simpler time, used primarily for file uploads but often tasked with carrying complex metadata alongside those files.
AdonisJS, the "Laravel of Node.js," prides itself on being robust and opinionated. But in versions of @adonisjs/bodyparser prior to 10.1.3, the bouncer missed a spot. The component responsible for aggregating those metadata fields had a classic JavaScript blindness: it trusted that a "new object" was actually empty.
This isn't just about reading a bad file; it's about memory corruption in a language that technically doesn't let you touch memory. By slipping a specific key into a form upload, an attacker can modify the DNA of every object in the running application. It's the digital equivalent of sneezing in the salad bar—suddenly, everyone has your cold.
To understand this bug, you have to understand the quirk at the heart of JavaScript: the Prototype Chain. When you create a simple object in JS using const obj = {}, it isn't empty. It comes pre-loaded with a hidden link (__proto__) to Object.prototype. This is why you can call .toString() on an empty object and it works—it inherits that method.
The vulnerability lived in the FormFields class. The parser's job is to take a stream of form parts and organize them. If I send a field named username with value admin, the parser adds username: 'admin' to its internal storage.
The fatal flaw was how that storage was initialized:
export class FormFields {
// The root of all evil
#fields: any = {}
}By initializing #fields as {} (a plain object), the developer inadvertently exposed the __proto__ accessor. When the parser logic blindly assigns values based on input keys (#fields[key] = value), an attacker supplying a key like __proto__ isn't setting a property on the #fields object itself—they are traversing the chain and modifying the global Object.prototype. This is Prototype Pollution 101, but seeing it in a major framework's core parsing logic is a stark reminder that const x = {} is rarely safe for user input.
The fix is elegantly simple, yet it highlights exactly why the bug existed. The maintainers didn't need to rewrite the parsing logic or add complex sanitization regexes (which are often bypassed anyway). They just needed to change the nature of the storage container.
Here is the critical diff from commit 40e1c71f958cffb74f6b91bed6630dca979062ed:
--- a/src/form_fields.ts
+++ b/src/form_fields.ts
@@ -17,7 +17,7 @@ export class FormFields {
/**
* Internal storage for form fields
*/
- #fields: any = {}
+ #fields: any = Object.create(null)> [!NOTE]
> Why does this work?
> Object.create(null) creates a dictionary object that has no prototype. It doesn't inherit from Object.prototype. It has no __proto__, no constructor, and no toString. If an attacker tries to access __proto__ on this object, the result is undefined, and the assignment simply creates a literal property named "__proto__" on that specific object, rather than traversing up to the global scope.
This is the definitive defense against prototype pollution for key-value stores in JavaScript. If you are storing user input in an object, and you aren't using a Map or Object.create(null), you are likely vulnerable.
Let's construct a Proof of Concept (PoC). We don't need authentication; we just need an endpoint that accepts multipart/form-data. This could be a profile picture upload, a contact form with attachments, or a resume drop.
The goal is to set a property on Object.prototype that the application relies on later. A classic target is isAdmin (for poor authorization checks) or gadgets in other libraries.
Here is how we formulate the request using curl:
curl -X POST http://localhost:3333/upload \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" \
--data-binary $'------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="__proto__[polluted]"\r\n\r\ntrue\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n'What happens inside the engine:
__proto__[polluted].#fields.__proto__['polluted'] = 'true'.#fields was a plain object, __proto__ points to the global Object.prototype.polluted set to true.If the application has code like if (user.isAdmin) { grantAccess() }, and the user object doesn't explicitly define isAdmin, it will look up the prototype chain. If we managed to pollute isAdmin to true, we just bypassed authentication for the entire platform.
While "Authentication Bypass" sounds scary, it relies on the application having loose logic. However, Denial of Service (DoS) is almost guaranteed with this vulnerability.
An attacker can overwrite standard methods like toString or valueOf. Imagine if every time the application tried to convert an object to a string (for logging, for database queries, for API responses), it crashed because toString was no longer a function but a string containing "pwned".
// The DoS Payload
name="__proto__[toString]"
value="hacked"One request with this payload brings the Node.js process to its knees. Since AdonisJS (like most Node apps) is single-threaded (or clustered), crashing the process repeatedly leads to total service unavailability.
Furthermore, if the application uses template engines or other libraries that merge objects deeply, this can lead to Remote Code Execution (RCE). This usually requires a "gadget chain"—code that takes our polluted property and passes it into eval() or child_process.exec(). While not inherent to the bug itself, the potential is always there in a complex dependency tree.
If you are running AdonisJS, check your @adonisjs/bodyparser version immediately. You are vulnerable if you are below 10.1.3 (v10 branch) or 11.0.0-next.9 (v11 branch).
Remediation Steps:
npm update @adonisjs/bodyparser or npm install @adonisjs/core@latest.package-lock.json to ensure the installed version matches the patched release.__proto__, constructor, or prototype in the body content. This catches the low-hanging fruit before it even hits your app logic.Developers should learn the lesson here: Never trust the default prototype. When building hashmaps or dictionaries in JavaScript that accept user keys, always use new Map() or Object.create(null).
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
@adonisjs/bodyparser AdonisJS | < 10.1.3 | 10.1.3 |
@adonisjs/bodyparser AdonisJS | < 11.0.0-next.9 | 11.0.0-next.9 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-1321 (Prototype Pollution) |
| CVSS v3.1 | 7.2 (High) |
| Attack Vector | Network (AV:N) |
| Authentication | None (PR:N) |
| EPSS Score | 0.00036 (10.29%) |
| Exploit Status | PoC Available |
| Patch Commit | 40e1c71f958cffb74f6b91bed6630dca979062ed |
The product receives input from an upstream component, but it does not restrict or incorrectly restricts the input from creating, modifying, or modifying the attributes of a prototype of the Base Object.