Understanding JSON Hijacking and JavaScript Arrays

JSON hijacking, a form of Cross-Site Script Inclusion (XSSI), is a historical web vulnerability that allowed malicious websites to steal sensitive data returned from cross-origin API endpoints. By abusing the way older web browsers parsed top-level JavaScript arrays and overriding native array constructors, attackers could bypass the Same-Origin Policy (SOP). This article breaks down the mechanics of how top-level JSON arrays were exploited and outlines the defensive techniques historically used to mitigate the vulnerability.

How the Vulnerability Worked

The Same-Origin Policy prevents a script loaded from one origin from directly reading the contents of a response from another origin via fetch or XMLHttpRequest. However, HTML tags such as <script> are exempt from this restriction to allow loading external libraries and resources.

JSON hijacking exploited this exemption through three main steps:

1. Top-Level Array Evaluation

In early JavaScript specifications, a standalone JSON object formatted as { "key": "value" } produced a syntax error if loaded inside a <script> tag because the JavaScript engine interpreted the curly braces as a code block rather than an object literal. In contrast, a top-level array such as [{"id": 1, "secret": "token"}] is a valid JavaScript array literal and evaluates without throwing a syntax error.

2. Bypassing the Same-Origin Policy

An attacker would host a malicious website containing a script tag pointing directly to the victim’s authenticated endpoint:

<script src="https://example.com/api/user-data.json"></script>

Because the browser automatically attached the victim’s session cookies to the request, the server returned the sensitive JSON array containing the authenticated user’s private data.

3. Intercepting the Data via Prototype Poisoning

Older JavaScript engines invoked the global Array constructor or prototype setters whenever an array literal was parsed. Attackers could redefine the Array constructor or use methods like Object.prototype.__defineSetter__ before importing the external script:

function Object() {
  // Capture properties as they are created
}

// Or override array behavior
var capturedData = [];
var originalArray = Array;
window.Array = function() {
  var arr = new originalArray();
  // Access and exfiltrate elements passed into the array
  return arr;
};

When the browser executed the payload inside the <script> tag, the engine triggered the attacker’s custom constructor or setter functions, exposing the contents of the array elements to the attacker’s exfiltration scripts.

Historical Mitigations

To neutralize this attack vector before browser vendors implemented structural fixes, developers adopted several defensive practices:

1. Wrapping Responses in Objects

Because standard object literals could not be evaluated directly by <script> tags without triggering a syntax error, APIs stopped returning top-level arrays. Instead, endpoints wrapped arrays inside parent objects:

{ "data": [ {"id": 1}, {"id": 2} ] }

When loaded via a <script> tag, the browser interpreted the outer braces as a block statement, resulting in a syntax error and preventing script execution.

2. JSON Parser Breakers (Infinite Loops and Prefixing)

Major platforms (such as Google and AngularJS applications) prepended executable scripts or syntax-breaking characters to the beginning of JSON responses. Common prefixes included:

Legitimate applications using XMLHttpRequest would read the raw string response, strip the prefix using string manipulation, and pass the cleaned string to JSON.parse().

3. Restricting Request Types and Headers

Because <script> tags can only perform GET requests and cannot set custom HTTP headers: * Sensitive endpoints were changed to require POST requests. * Servers required custom headers (such as X-Requested-With: XMLHttpRequest) or CSRF tokens before returning data, which standard cross-origin script tags could not supply.

4. Modern Browser Protections

The ECMAScript 5 specification resolved the core issue at the engine level. Modern browsers create array literals directly through internal engine routines rather than calling user-definable constructors or invoking prototype setters during array literal parsing. As a result, overriding Array or Object.prototype no longer intercepts literal evaluation.