Why this matters for PCI DSS
The classic JSON hijacking attack worked like this in the late 2000s:
- Your authenticated user visits
evil.com.
evil.com includes <script src="https://your-bank.com/api/transactions.json"></script>.
- The browser fetches the URL with the user's cookies. The response is a top-level JS array.
evil.com has overridden the global Array constructor (or the array index setter) to log every value, then sends them to the attacker.
Modern browsers fixed the Array-constructor trick around 2011, but new variants still appear (see the AngularJS-era variant, JSONP-injection variants, and SOP edge cases in the Securitum research). For an attacker investing time on a high-value target, "old" attack classes are very much alive.
PCI DSS 4.0 Requirement 6.2.4 covers cross-site data leakage broadly. The fix is cheap, so there's no reason not to apply it.
How to fix it
Pick any one — they all defeat JSON hijacking:
1. Wrap top-level arrays in an object. Smallest code change with the broadest compatibility:
// BAD
[{ "id": 1, "amount": 100 }, { "id": 2, "amount": 200 }]
// GOOD
{ "transactions": [{ "id": 1, "amount": 100 }, { "id": 2, "amount": 200 }] }
2. Prefix the response with an unparseable token that your client strips before JSON.parse. The Google convention:
)]}',
[{ "id": 1, "amount": 100 }]
Client side: JSON.parse(response.slice(5)).
This makes the response invalid JavaScript when loaded as a <script> (so the attack fails) but trivial to strip when loaded as JSON via fetch/XHR.
3. Require a custom header. Reject requests that don't carry X-Requested-With: XMLHttpRequest or a custom header your frontend sets. <script src> can't set custom headers, so the attack request gets a 400 instead of the data:
// Server (Express):
app.get('/api/transactions', (req, res) => {
if (req.get('X-Requested-With') !== 'XMLHttpRequest')
return res.status(403).end();
res.json(transactions);
});
4. Require POST instead of GET for any data fetch. <script src> only issues GET, so a POST-only endpoint is immune.
5. SameSite cookies (covered in Check 14) — SameSite=Lax or Strict stop authentication cookies from being sent on the cross-origin <script src> request. The attack's data, if it leaks, is unauthenticated — usually harmless. This is the cheapest defense and a good belt-and-suspenders measure even if you also do (1) or (2).
The combination of "wrap arrays in objects" plus "SameSite=Lax cookies" eliminates JSON hijacking as a practical concern with essentially zero engineering effort.