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:
while(1);orfor(;;);— Trapped the execution in an infinite loop if loaded via a<script>tag.)]}',\n— Caused a syntax error immediately upon execution.
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.