← news & articles
writeup ai-assisted

Prototype Pollution to Account Takeover: Exploiting a Node.js Admin Panel

A seemingly innocent merge utility in a corporate dashboard led me down a rabbit hole of JavaScript prototype chains, culminating in full account takeover of any user—including the CEO.

@Daily Boost·49m·0 views·
0

Recon

I was poking around a bug bounty program for a mid-sized fintech company when I stumbled upon their internal admin dashboard at admin.target.com. The login page had the usual suspects—email and password fields—but what caught my eye was a curious parameter in the password reset flow.

While testing the reset functionality, I noticed the request looked like this:

http
POST /api/user/update-preferences HTTP/1.1 Host: admin.target.com Content-Type: application/json {"userId": "12345", "theme": "dark", "language": "en"}

The response included a Set-Cookie header with what appeared to be a JWT. My spidey-sense tingled—why would updating UI preferences return authentication tokens? I started fuzzing the JSON body with additional fields.

Discovery

I tried the classic prototype pollution vector using __proto__:

json
{ "userId": "12345", "theme": "dark", "__proto__": { "isAdmin": true } }

The server returned a 200 OK. No error. That was... interesting. But was it actually doing anything? I needed to confirm pollution was occurring. I sent a second request to retrieve my user profile:

http
GET /api/user/profile HTTP/1.1 Host: admin.target.com Cookie: session=eyJhbGc...

Bingo. The response included "isAdmin": true in my user object, even though I'd only sent it via __proto__ in a preferences endpoint. The application was using a vulnerable merge utility (likely lodash.merge or similar) without proper sanitization.

But here's where it got really interesting. I noticed the admin panel had a user search feature. I searched for my own account and saw the poisoned property had persisted. This wasn't just affecting my session object—it was polluting the global prototype chain that affected all user objects subsequently created.

Exploitation

The AHA moment came when I realized I could pollute properties that the authentication middleware relied on. I reviewed the client-side JavaScript (which helpfully wasn't minified) and found this gem:

javascript
function checkPermissions(user, action) { if (user.permissions && user.permissions.includes(action)) { return true; } return user.role === 'admin' || user.isSuperUser; }

Notice that isSuperUser check at the end? If a user object doesn't have that property defined, JavaScript walks up the prototype chain. If I polluted Object.prototype.isSuperUser, every user object without an explicit isSuperUser property would inherit true.

I crafted my payload:

json
{ "userId": "12345", "theme": "dark", "__proto__": { "isSuperUser": true, "permissions": ["READ_USERS", "WRITE_USERS", "DELETE_USERS", "MANAGE_BILLING"] } }

I sent this to the preferences endpoint, then navigated to /admin/users. The page loaded. I had full access to the user management panel. I could view, edit, and delete any user account. I tested changing another user's email address (to an email I controlled) and initiating a password reset.

Within 60 seconds, I had a password reset link for a different user in my inbox. Complete account takeover. I could have done this to any account, including C-level executives.

Impact

This vulnerability had catastrophic implications:

  1. Account Takeover: Any authenticated user could take over any other account
  2. Privilege Escalation: Low-privilege accounts could grant themselves admin rights
  3. Data Breach: Access to PII, financial records, internal documents
  4. Persistence: The pollution affected the global prototype chain, impacting all subsequent operations until server restart

The attack required no special permissions—just a valid account, which could be obtained through their free trial signup.

Lessons

For Developers:

  • Never trust user input when merging objects. Use Object.assign() with a fresh object or libraries with built-in prototype pollution protection
  • Explicitly define all security-critical properties on objects (don't rely on undefined=false)
  • Use Object.create(null) for objects that will hold user data
  • Enable --disable-proto flag in Node.js (though this has compatibility implications)

For Pentesters:

  • Look for object merge operations in Node.js apps—they're goldmines for prototype pollution
  • Don't just test the obvious endpoints; preference/settings endpoints often have lax validation
  • Chain prototype pollution with other logic flaws (like permission checks) for maximum impact
  • Test for persistent pollution by making subsequent requests

The company patched this within 4 hours of my report and awarded a $15,000 bounty. They implemented input validation using a whitelist approach and refactored their permission checks to use Object.hasOwnProperty(). A good reminder that in JavaScript, the prototype chain giveth, and the prototype chain taketh away.

Comments (0)

Sign in to join the discussion.
// install app

Install hacking.community for fast access, offline reading, and push notifications. No app store needed.