VNX-1321: Prototype pollution through unguarded computed-key assignment
Detects JavaScript/TypeScript writes through a key an attacker can set to __proto__ or constructor: hand-written recursive merges, set-by-path walks, and nested assignments keyed by request data, with no key guard.
| Field | Value |
|---|---|
| Rule ID | VNX-1321 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | JavaScript / TypeScript |
| CWE | CWE-1321 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | prototype-pollution cwe-1321 |
| Source | internal/sast/rules/vnx-1321.rego |
Overview
VNX-1321 maps to CWE-1321: Improperly Controlled Modification of Object Prototype Attributes (‘Prototype Pollution’). When code writes target[key] for a key taken from untrusted data, the key __proto__ (or constructor then prototype) makes the write land on Object.prototype, and every object in the process inherits the injected property: authorisation flags, template options, child-process options.
The rule reports three shapes:
- A recursive merge or extend: inside a
for...in,for...of Object.keys/getOwnPropertyNames/entries(...)orReflect.ownKeys(...)orObject.keys(...).forEachloop over a source object, a call that passestarget[key]together withsource[key](deepMerge(target[key], source[key]),extend(dst[k] || {}, src[k]),Object.assign(t[k], s[k])). - A set-by-path walk: a loop that walks
cur = cur[part]followed within a few lines bycur[...] = value. - A nested assignment keyed by request data:
obj[A][B] = vorobj[A].x = vwhose first key comes fromreq.body/query/params/cookies/headers,ctx.queryorctx.request.body, directly or through a variable assigned from it in the 10 lines above.
Not reported: code whose surrounding 15 lines name __proto__, constructor or prototype, use Object.create(null) or a Map, or call a safe-key check; a shallow one-level copy; nested assignments with constant keys or loop indices; comments and strings. hasOwnProperty is deliberately not a guard: JSON.parse gives an own __proto__ key. Library merges called with request data are VNX-NODE-006.
What Gets Flagged
function deepMerge(target, source) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) target[key] = {};
deepMerge(target[key], source[key]); // FLAGGED
} else {
target[key] = source[key];
}
}
}
router.post('/settings', (req, res) => {
store[req.body.section][req.body.key] = req.body.value; // FLAGGED
});
What Is Not Flagged
const UNSAFE = new Set(['__proto__', 'constructor', 'prototype']);
for (const key in source) {
if (UNSAFE.has(key)) continue;
deepMerge(target[key], source[key]);
}
Remediation
- Skip
__proto__,constructorandprototypekeys in every merge, extend and set-by-path helper. - Merge into
Object.create(null)objects, or use aMapfor data keyed by user input. - Validate request-supplied keys against an allowlist before using them as property names.
- Consider
Object.freeze(Object.prototype)at startup as defence in depth.
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-1321
// nosec vnx-1321
Or record an ignore rule: vulnetix ignore add --rule vnx-1321 --reason "...". See Suppressing findings.