Affected Versions
| Affected | unpatched versions prior to harden commit |
| Fixed in | harden: use safe deserialization in load_localStorage.js (CWE-502) |
| Ecosystem | not applicable (first-party code) |
| CVE / GHSA | not assigned |
| CWE | unknown (CWE-502 referenced in PR) |
The Vulnerability Explained
The loadJsonAndStoreInLocalStorage() function fetched and parsed JSON data using a bare response.json() call:
cachedData = await response.json();
This direct assignment created a vulnerability surface. When the parsed data contained keys like __proto__, constructor, or prototype, JavaScript's object property resolution would traverse the prototype chain. The subsequent storage via localStorage.setItem() and downstream processing could propagate this pollution throughout the application.
Consider an attacker who gains write access to localStorage.json (through path traversal, compromised build pipeline, or local filesystem manipulation). They could inject:
{"__proto__": {"polluted": true}, "legitimate": "data"}
When parsed and processed, this doesn't merely set a property named "__proto__" — in many JavaScript contexts, it can modify Object.prototype.polluted, affecting every object created afterward. The cachedData variable, populated directly from the parse result, became a vehicle for prototype pollution that could alter application behavior far from the ingestion point.
The real-world impact depends on how the stored data is consumed. If downstream code checks for properties without hasOwnProperty() guards, or if the polluted prototype affects internal JavaScript operations, the attacker gains subtle but pervasive control over execution flow.
The Fix
The fix introduces defense-in-depth through explicit validation and bounded reconstruction:
Before:
cachedData = await response.json();
After:
const parsedData = await response.json();
if (typeof parsedData !== 'object' || parsedData === null || Array.isArray(parsedData)) {
throw new Error('Invalid localStorage.json: expected a plain object');
}
cachedData = {};
for (const key of Object.keys(parsedData)) {
if (isUnsafeKey(key)) continue;
cachedData[key] = parsedData[key];
}
The isUnsafeKey() function provides the primary security boundary:
function isUnsafeKey(key) {
return key === '__proto__' || key === 'constructor' || key === 'prototype';
}
This change solves the problem through three specific mechanisms:
-
Schema validation: The type check ensures
parsedDatais a plain object, rejecting arrays and null values that would break downstream expectations. -
Key filtering: By iterating
Object.keys()rather than using object spread or direct assignment, the code avoids triggering prototype chain lookups during property access. -
Object reconstruction: Creating a fresh
cachedDataobject with only explicitly allowed keys isolates the application from any prototype pollution that might exist in the parsed structure itself.
The continue statement on unsafe keys silently drops them — a deliberate choice that prevents injection without crashing legitimate operation.
Key Takeaways
- Never assign parsed JSON directly to variables used across security boundaries — always validate structure and filter keys against prototype-reserved names.
__proto__,constructor, andprototypeare unsafe as object keys in any context where the object will be used with standard property access patterns.- Object reconstruction beats sanitization — building a new object with known-safe keys is more reliable than attempting to delete or modify properties on a potentially polluted object.
- Defense-in-depth at deserialization boundaries prevents cascading failures when upstream trust assumptions break down.
How Orbis AppSec Detected This
Source: The response.json() call ingesting data from the network or filesystem
Sink: The cachedData variable subsequently used with localStorage.setItem() and downstream property access
Missing control: No validation of parsed structure or filtering of prototype-reserved key names (__proto__, constructor, prototype)
CWE: CWE-502 (Deserialization of Untrusted Data) — though the PR notes this as defense-in-depth rather than an exploitable vulnerability in this specific context
Fix: Added isUnsafeKey() predicate with explicit object reconstruction, rejecting __proto__, constructor, and prototype keys at the deserialization boundary
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This fix demonstrates how a small deserialization utility can become a critical security boundary. The loadJsonAndStoreInLocalStorage() function's direct assignment pattern — common in JavaScript codebases — silently accepted prototype pollution vectors. The explicit isUnsafeKey() filter and bounded object reconstruction transform an implicit trust assumption into an explicit security boundary, making the failure mode "fail-closed" rather than "fail-polluted."