~/teardowns/TD-041.md
←Back to teardowns
# TD-041HighSecurity

One Request That Changed Every Object

Deep-merging query params into shared config pollutes the prototype

Published Oct 29, 2026

Executive Summary

A vibe-coded Express app deep-merges query parameters into its shared config object so users can override a few settings. Because the merge is recursive and unguarded, a request containing __proto__ pollutes Object.prototype — and suddenly isAdmin is truthy everywhere. One visitor changes the app for everyone.

The Prompt

“Let users override a few app settings (theme, locale) by passing them as query parameters.”

The Flaw

A recursive merge that walks arbitrary keys copies req.query's own properties into config — including the magic key __proto__. Setting __proto__.polluted (or constructor.prototype.isAdmin) mutates Object.prototype, so every object in the process inherits the injected property. One crafted request flips security checks app-wide, and the pollution survives until the process restarts.

Risk & Impact

[Security] A single request can make authorization checks pass everywhere, because 'if (user.isAdmin)' now finds an inherited truthy property on every object.

Cost: App-wide security bypass — and the cause is nearly impossible to find by reading the request handler alone.

Code Comparison

❌ Vulnerable (AI generated)

config.jsjavascript
1
2
3
4
5
6
7
const merge = require('lodash.merge');
app.get('/', (req, res) => {
merge(config, req.query); // user input into the shared config object
res.json({ theme: config.theme });
});

The Fix

Never deep-merge untrusted input into a shared object. Pick known keys into a fresh object, and freeze or guard prototypes where merging is unavoidable.

Prevention

  • [linter] ESLint security plugins flag lodash.merge/_.merge and recursive extend on request data.
  • [ci] A test that sends __proto__ or constructor.prototype as a query param must leave Object.prototype untouched — assert on a clean object's inherited props.
  • [manual] Require an explicit key allowlist for any user-supplied object (rule 3), and forbid merging request data into globals (rule 6).

Why This Happened

Root cause: the prompt omitted Rule 3 and Rule 6 — the guardrails that would have caught this. See the full guide →

Sources

Is your codebase vulnerable to this?

Run Diagnostic on My Project →