A JavaScript service counts events with `counts[key] = (counts[key] || 0) + 1` on an object literal, where `key` comes from user input. What can go wrong, and how would you make it safe?
answer
- the store is not empty when you create it
- some keys already have values
- one key is not a key at all
- no prototype, no problem
- a Map has no inherited names
basics
~20 sAn object literal inherits properties from Object.prototype, so keys like 'toString' or 'constructor' read back as inherited functions instead of undefined, and assigning 'proto' changes the object's prototype rather than adding an entry. Use a Map, or Object.create(null).
solid answer
~40 sThe object is not empty to begin with — it inherits from `Object.prototype`. If a user sends `toString`, then `counts['toString']` reads the inherited function, which is truthy, so `(fn || 0) + 1` concatenates the function's source with `1` and stores a string instead of a number. `constructor`, `valueOf` and `hasOwnProperty` behave the same way, and `hasOwnProperty` being overwritten breaks later guard checks. Worse, `counts['__proto__'] = value` does not create an entry at all — it invokes the inherited setter and replaces the object's prototype, which is the classic prototype-pollution vector. The fixes, in order of preference: use a `Map`, whose keys never touch a prototype; or create the store with `Object.create(null)`, which has no inherited properties and no `__proto__` accessor; or, if you must keep a literal, guard every read with `Object.hasOwn(counts, key)`.
code
javascript · 16 linesconst counts = {};
counts['toString'] = (counts['toString'] || 0) + 1;
console.log(typeof counts.toString); // 'string', not 'number'
const raw = {};
raw['__proto__'] = { admin: true };
console.log(Object.keys(raw)); // [] — no entry created
console.log(raw.admin); // true — prototype was replaced
const safe = Object.create(null);
safe['toString'] = (safe['toString'] || 0) + 1;
console.log(safe.toString); // 1
const m = new Map();
m.set('toString', (m.get('toString') ?? 0) + 1);
console.log(m.get('toString')); // 1go deeper
Know that an object literal inherits from Object.prototype, so names like toString and constructor already read as values, and that a Map has no such inherited keys.
Walk through the concrete failure: the inherited function is truthy, (fn || 0) + 1 produces a string, and __proto__ assignment hits an inherited setter instead of creating an entry. Name Map, Object.create(null), and Object.hasOwn as fixes.
Show how you would find this in a running system — string counts, assignments that leave no key, unrelated objects gaining properties — and argue for Map as the default container for untrusted keys rather than sprinkling guards at each access site.
Treat it as an input-boundary rule: decide once that externally supplied keys never index an ordinary object, and make deep-merge and dictionary helpers enforce that so no future contributor has to remember the guard.
## Why an "empty" object is not empty `const counts = {}` creates an object whose prototype is `Object.prototype`. Property reads fall back to that prototype, so before you store anything the object already answers to a set of names: ```js const counts = {}; console.log(counts['toString']); // function toString() { [native code] } console.log('constructor' in counts); // true console.log(counts.valueOf); // function ``` That is fine for a record with known fields. It is a bug for a dictionary whose keys come from outside, because the key namespace you thought was empty is pre-populated. ## What the counter actually does Take `key === 'toString'`: ```js const counts = {}; const key = 'toString'; counts[key] = (counts[key] || 0) + 1; console.log(typeof counts.toString); // 'string' console.log(counts.toString); // 'function toString() { [native code] }1' ``` The read finds the inherited function. A function is truthy, so `|| 0` never fires. Adding a number to a function converts the function to its source text and concatenates. You now hold a string where every other entry is a number, and you have shadowed `toString` on this object, so anything that later stringifies `counts` throws a TypeError because `toString` is no longer callable. The same shape of failure hits `constructor`, `valueOf`, `hasOwnProperty`, and `isPrototypeOf`. If your code guards with `if (counts.hasOwnProperty(key))` and a user has stored a value under `hasOwnProperty`, the guard itself throws. ## The `__proto__` case is different and worse `__proto__` is an accessor defined on `Object.prototype` with a getter and a setter. Assigning through it does not create an own property; it calls the setter and changes the object's prototype: ```js const counts = {}; counts['__proto__'] = { admin: true }; console.log(Object.keys(counts)); // [] — no entry was created console.log(counts.admin); // true — inherited from the new prototype ``` If the assigned value is not an object or `null`, the setter silently ignores it, so the write vanishes without error. Either way the entry you expected is missing, and in the object case you have handed an attacker influence over the prototype chain of that object. When the same pattern is applied to a shared object — a deep-merge helper walking untrusted JSON into a config object, for example — this is prototype pollution: properties land on `Object.prototype` and become visible on *every* ordinary object in the program. ## How to diagnose it The symptom is usually weird rather than loud: a count that is a string, a key that never appears in `Object.keys` despite an assignment that did not throw, or an unrelated part of the app suddenly seeing a property it never set. Reproduce it by feeding the literal names — `toString`, `constructor`, `__proto__`, `hasOwnProperty` — through whatever path supplies keys. If those inputs behave differently from ordinary keys, you have this bug. ## The fixes **Use a Map.** A `Map` has no key inheritance at all; `'__proto__'` and `'toString'` are just strings with no special meaning to it: ```js const counts = new Map(); counts.set(key, (counts.get(key) ?? 0) + 1); ``` This is the answer to give first. It also fixes the reading side, because `get` returns `undefined` for anything never set, and `has` distinguishes "absent" from "present with a falsy value". **Use a null-prototype object** when you specifically want a plain object — for example because the result is serialized immediately: ```js const counts = Object.create(null); counts['__proto__'] = 5; // an ordinary own property now console.log(Object.keys(counts)); // [ '__proto__' ] ``` With no `Object.prototype` in the chain there are no inherited names and no `__proto__` accessor. The tradeoff is that the object has no `toString`, `hasOwnProperty`, or other helpers, so it prints awkwardly in some tools and you must call `Object.prototype.hasOwnProperty.call(o, k)` rather than `o.hasOwnProperty(k)`. **Guard every read** if you are stuck with a literal: `Object.hasOwn(counts, key)` (ES2022) or `Object.prototype.hasOwnProperty.call(counts, key)` before trusting a value. This is the weakest option, because it only works if *every* access site remembers, and it does not stop the `__proto__` assignment. ## The point of the question An interviewer is checking whether you treat a plain object as a safe container for untrusted keys. The strong answer names the mechanism — inherited properties plus the `__proto__` accessor — before naming the fix, and reaches for `Map` as the default rather than adding guards to a structure that was never meant for the job.
- Does `JSON.parse` protect you here, since the parsed object comes from a string?No. `JSON.parse('{}')` returns an ordinary object with `Object.prototype` in its chain, so it has the same inherited names. It does treat a `"__proto__"` member as an own data property rather than invoking the setter, but the danger returns the moment you copy that object's keys onto another one with a merge or a `for...in` loop.
- What do you give up by using `Object.create(null)`?Every inherited helper: no `toString`, no `hasOwnProperty`, no `valueOf`. String concatenation and some logging paths throw or print oddly, and membership checks must go through `Object.prototype.hasOwnProperty.call` or `Object.hasOwn`. It is a good dictionary and a poor general object, which is why a Map is usually the cleaner choice.
- Why prefer `has`/`get` on a Map over checking for `undefined` on an object?Because `undefined` is ambiguous: an object read cannot distinguish "never set" from "set to undefined", and on a literal it also cannot distinguish either from "inherited". `map.has(key)` answers membership exactly, and `map.get(key)` returns `undefined` only for keys genuinely absent from that Map.
saying these in an interview costs you the question
- Says an object literal starts with zero properties
- Thinks obj['__proto__'] = x adds a normal entry
- Claims JSON.parse output is immune to inherited keys
- Suggests only renaming user keys with a prefix as the fix
- Uses obj.hasOwnProperty(key) as a guard without noting it can be shadowed