This module is about preventing common security failures in shared JavaScript environments. It intentionally focuses on detection, mitigation, and secure coding patterns, not exploit development.
Typical shared-runtime scenarios:
- multi-tenant backend services
- plugin/extension architectures
- user-provided JSON payloads
- shared in-memory caches/state objects
A trust boundary appears whenever data crosses from less-trusted to more-trusted code. Common boundary points:
- HTTP request body parsing
- message queue payloads
- plugin API inputs
- config/feature flag ingestion
"Parsed" does not mean "trusted". JSON parsing gives structure, not safety.
Security incidents often originate from:
- internal utility functions reused in unintended contexts
- assumptions that all callers are trusted
- shared helpers (
merge,clone,normalize) used on untrusted data later
Defensive defaults in utility code prevent cross-team accidental vulnerabilities.
Prototype pollution is unintended mutation of language-level prototypes, commonly:
Object.prototypeArray.prototype- function prototype objects
If polluted, many unrelated objects inherit attacker-controlled behavior/values.
- deep merge utilities
- deep assignment by path
- querystring-like object expansion
- config merge code that trusts keys
Always treat these as dangerous in untrusted input:
"__proto__""prototype""constructor"
- logic changes from inherited polluted defaults
- authorization checks reading unexpected inherited fields
- denial-of-service through global behavior corruption
- Block dangerous keys in deep merge/setter code.
- Prefer
Object.create(null)for dictionary maps. - Use own-property checks (
Object.hasOwn,Object.prototype.hasOwnProperty.call). - Freeze or seal trusted config objects where appropriate.
- Keep structured cloning boundaries explicit (copy data, not behavior/prototype links).
'use strict';
const DANGEROUS = new Set(['__proto__', 'prototype', 'constructor']);
function isPlainObject(v) {
return Object.prototype.toString.call(v) === '[object Object]';
}
function safeDeepMerge(target, source) {
for (const key of Object.keys(source)) {
if (DANGEROUS.has(key)) continue;
const next = source[key];
if (Array.isArray(next)) {
target[key] = next.slice();
continue;
}
if (isPlainObject(next)) {
const base = isPlainObject(target[key]) ? target[key] : {};
target[key] = safeDeepMerge(base, next);
continue;
}
target[key] = next;
}
return target;
}JavaScript exposes reflective and dynamic features:
- constructors and dynamic evaluation primitives
- global object references and ambient authority
- shared runtime capabilities unless explicitly constrained
A "sandbox" that only filters source text is not enough.
- capability leakage (access to powerful APIs)
- ambient authority (implicit access to process/global state)
- global mutation leakage across tenants/plugins
- isolated OS processes/containers
- workers with strict message-passing boundaries
- policy-based allowlists for capabilities
- least-privilege API surfaces (explicit capability objects)
The defensive goal is containment + explicit authority, not ad-hoc string filtering.
'use strict';
const dict = Object.create(null);
dict['toString'] = 'ok';No inherited prototype keys reduces collision/surprise from lookup semantics.
'use strict';
function parseWithSchema(input, schema) {
const out = {};
for (const key of schema.allowedKeys) {
if (Object.hasOwn(input, key)) out[key] = input[key];
}
for (const req of schema.requiredKeys || []) {
if (!Object.hasOwn(out, req)) throw new Error(`Missing required: ${req}`);
}
return out;
}JSON.parse(JSON.stringify(x))- drops functions/symbols/undefined
- loses special object types
- structured clone
- broader type support
- still not a security validator by itself
- custom deep clone
- must handle edge cases and dangerous keys explicitly
Object.freeze is shallow by default.
Deep immutability requires recursive freezing or immutable data architecture.
- optional chaining helps null safety (
obj?.a?.b) - own checks help trust safety (
Object.hasOwn(obj, 'k'))
Optional chaining does not prevent inherited polluted values.
- Do not trust property lookups on untrusted objects.
- Prefer
Object.hasOwn(...)or.hasOwnProperty.call(...). - Avoid raw
for..inon untrusted objects unless guarded with own checks. - Avoid unnecessary prototype-walking operations.
- Use safe merge and explicit allowlists.
- Serialize only required fields.
'use strict';
function createSafeDict() {
const store = Object.create(null);
return {
set(k, v) { store[k] = v; },
get(k) { return store[k]; },
has(k) { return Object.prototype.hasOwnProperty.call(store, k); },
keys() { return Object.keys(store); },
};
}'use strict';
const DANGEROUS = new Set(['__proto__', 'prototype', 'constructor']);
function validateAllowedKeys(input, allowed) {
for (const key of Object.keys(input)) {
if (DANGEROUS.has(key)) throw new Error(`Dangerous key: ${key}`);
if (!allowed.has(key)) throw new Error(`Unexpected key: ${key}`);
}
}A concise answer:
- Untrusted keys flow into deep merge/setter utilities.
- Dangerous keys (
__proto__,constructor,prototype) can mutate prototype-related behavior. - This can alter unrelated logic through inherited properties.
- Fix with key filtering, own-property checks, null-prototype maps, and strict schema allowlists.
- recursive merge without key filtering
- direct
obj.hasOwnProperty(...)on untrusted objects for..inwithout own-guard- broad object spreading of untrusted input
- caches/config maps backed by plain
{}when key space is untrusted
- keep a small shared safe-merge helper
- provide schema-based parsers per boundary
- encapsulate dictionary usage behind safe API
- fail fast with clear validation errors
- add tests for dangerous keys as regression guards
- "Only external code can cause security bugs."
- "JSON.parse sanitizes malicious keys."
- "Optional chaining solves prototype pollution."
- "for..in is fine if data came from our own API."
- "Freezing one top-level object fully secures config."
Use this quick checklist during code review:
- Does merge/setter logic reject
__proto__,prototype,constructor? - Are untrusted objects accessed via own-property checks?
- Are dictionaries using
Object.create(null)where key space is untrusted? - Is
for..inguarded with own checks (or replaced)? - Are unknown keys stripped at trust boundaries?
- Are required keys validated as own properties?
- Are arrays handled intentionally in merge logic (replace vs merge)?
- Is prototype mutation behavior tested explicitly?