Vulnerability Report: Prototype Pollution to Access Control Bypass
Executive Summary
The target application is vulnerable to Prototype Pollution due to insecure handling of the MongoDB $rename operator via the Mongoose ODM (Object Data Modeling) library. By manipulating database documents to include a __proto__ key and subsequently triggering Mongoose's document hydration process, an attacker can pollute the global Object.prototype.
This pollution can be leveraged to manipulate internal Node.js net.Socket properties, effectively bypassing the IP-based access control on the /flag endpoint.
1. The Core Vulnerability: Mongoose Hydration
When Mongoose reads a document from the MongoDB database, it converts the raw BSON data into a JavaScript object (a Mongoose Document). This process is called hydration.
Historically, Mongoose has been vulnerable to prototype pollution if a database document contains a __proto__ key. During hydration, instead of assigning __proto__ as a standard property, the JavaScript engine interprets it as a setter for the object's prototype, inadvertently modifying the global Object.prototype.
2. Bypassing Schema Defenses with $rename
Mongoose schemas are strict by default. If we attempt to inject {"__proto__": ...} directly via the /create endpoint, Mongoose filters it out because __proto__ is not defined in the Note schema (title and content only).
To bypass this, the exploit uses the /update endpoint, which accepts arbitrary MongoDB operators. By using the $rename operator:
-
We inject the malicious payload into a valid schema field (
content). -
We issue a
$renamecommand to alter the key name directly in the database to__proto__._peername.address. -
This alters the raw MongoDB document behind Mongoose's back, bypassing the schema validation.
3. The Shadowing Problem: Why remoteAddress Fails
The application secures the /flag endpoint using the following check:
JavaScript
A standard prototype pollution attack would target Object.prototype.remoteAddress. However, in Node.js, req.connection is an instance of net.Socket. The remoteAddress property is not a static string; it is a getter defined on net.Socket.prototype.
When the application evaluates req.connection.remoteAddress, the JavaScript engine stops traversing the prototype chain as soon as it hits the getter on net.Socket.prototype. It executes the getter, which returns the actual IP, completely ignoring any polluted value sitting further down the chain on Object.prototype. This is known as property shadowing.
4. The Exploit Mechanism: Bypassing the Getter
To bypass the shadowed property, the exploit targets the internal logic of the getter itself. The native Node.js remoteAddress getter retrieves its value from an internal object called _peername:
JavaScript
The successful exploit chain works as follows:
-
Pollute the internal property: We use the
$renamegadget to polluteObject.prototype._peername = { address: "::ffff:127.0.0.1" }. -
Force a fresh connection: We drop the HTTP Keep-Alive connection and request the
/flagendpoint with a brand new socket. -
Trigger the fallback: On a new or half-open socket, the
net.Socketinstance may not have its internal_peernameproperty initialized immediately. -
The Bypass: When the getter executes,
this._peernameis undefined on the instance. The engine traverses the prototype chain, finds our pollutedObject.prototype._peername, and extracts the spoofed address, granting access to the flag.