Back to Blog
high SEVERITY3 min read

load_localStorage.js JSON Parse: Prototype Pollution via __proto__

The load_localStorage.js utility parsed JSON configuration without validating keys, permitting prototype pollution through malicious `__proto__`, `constructor`, or `prototype` properties. An attacker with filesystem access could poison downstream JavaScript execution by injecting these special keys into the loaded data structure.

O
By Orbis AppSec
•Published September 25, 2026•Reviewed September 25, 2026

Answer Summary

The load_localStorage.js JSON loader in unpatched versions accepts arbitrary object keys without validation. An attacker with write access to localStorage.json can inject `__proto__` properties that pollute Object.prototype, affecting all subsequent object operations in the browser context. The fix introduces an `isUnsafeKey()` filter and explicit object reconstruction at the deserialization boundary. CWE-502 (Deserialization of Untrusted Data).

Vulnerability at a Glance

cweunknown (CWE-502 referenced in PR)
fixExplicit key filtering with isUnsafeKey() check and bounded object reconstruction
riskPrototype pollution poisoning application-wide object behavior
languageJavaScript
root causeDirect assignment of parsed JSON keys without filtering prototype-reserved names
vulnerabilityUnsafe JSON deserialization with prototype pollution

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:

  1. Schema validation: The type check ensures parsedData is a plain object, rejecting arrays and null values that would break downstream expectations.

  2. Key filtering: By iterating Object.keys() rather than using object spread or direct assignment, the code avoids triggering prototype chain lookups during property access.

  3. Object reconstruction: Creating a fresh cachedData object 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, and prototype are 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."

Prevention and further reading

Frequently Asked Questions

Why does the fix reconstruct `cachedData` as a new object rather than just deleting unsafe keys from the parsed result?

Deleting `__proto__` from an object doesn't remove the prototype chain itself; creating a fresh object with only safe keys guarantees isolation from the original polluted structure.

Which three specific strings trigger the `isUnsafeKey()` filter?

`__proto__`, `constructor`, and `prototype` — all properties that can reach Object.prototype when used as object keys.

Does the `Array.isArray()` check in the fix prevent any exploitable behavior, or is it defense-in-depth?

It prevents a class of errors where downstream code expects object methods (like `Object.keys()`) on what would become array-indexed data, but the primary security boundary is the `isUnsafeKey()` filter.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #168

Related Articles

critical

How Arbitrary Code Execution Happens in protobufjs and How to Fix It

CVE-2026-41242 is a critical vulnerability in protobufjs versions 8.0.0 and earlier that allows attackers to execute arbitrary code by injecting malicious type fields into protobuf definitions. The fix upgrades the dependency from `^8.0.0` to `^8.6.6` in `core/package.json`, eliminating the unsafe code path that processed attacker-controlled type metadata without validation.

high

How Quadratic CPU Consumption Happens in JS-YAML and How to Fix It

A critical vulnerability in JS-YAML versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through maliciously crafted YAML input using the `!!omap` tag resolver. The vulnerability stems from inefficient array operations in the ordered map resolution logic, which could be exploited for denial-of-service attacks. Upgrading to JS-YAML 4.3.1 or 3.15.1 patches this attack surface by optimizing the computational complexity of ordered map processing.

critical

How Type Confusion Vulnerabilities Happen in JavaScript Dependencies and How to Fix Them

A critical type confusion vulnerability (CVE-2021-23436) was discovered in immer 9.0.7, a popular immutable state management library used in the client application. By upgrading to immer 9.0.6, the vulnerability was patched, eliminating a flaw that could have allowed attackers to bypass previous security fixes (CVE-2020-28477). This fix demonstrates why keeping dependencies current is essential for maintaining application security.

critical

How Prototype Pollution Happens in i18next-fs-backend and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-48713) was discovered in i18next-fs-backend versions prior to 2.6.6, where specially crafted missing-key strings could pollute the JavaScript object prototype. This fix upgrades the dependency to patch the vulnerability and prevent attackers from injecting malicious properties into application objects.

critical

How Prototype Pollution Happens in JavaScript Carousel Libraries and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-27212) was discovered in Swiper versions up to 11.2.10, a popular JavaScript carousel library used in production web applications. This vulnerability could allow attackers to manipulate application behavior through the prototype chain. The fix involved upgrading Swiper from 11.2.10 to 12.1.2, which patches the underlying prototype pollution flaw.

high

brace-expansion DoS: Exponential Backtracking in Nested Brace Patterns

A critical vulnerability in brace-expansion allows attackers to cause denial of service by submitting specially crafted patterns with nested braces. The exponential-time complexity in pattern expansion creates a computationally expensive path that can freeze applications processing user-controlled input.