Emmanuel Corels
← Blog
Cybersecurity

Serve private JSON without relying on a legacy infinite-loop prefix

Understand why sites prefixed JSON with an infinite loop, then replace that historical patch with explicit authorization, MIME, CORS, cookie, CSRF, cache, and parsing controls.

By Emmanuel Corels

An API response begins with while(1);, so pasting it into a JSON validator fails. The prefix is intentional: old web applications used it to make a response invalid as executable JavaScript, frustrating historical JSON-hijacking techniques. It is a compatibility convention, not authentication, and it does not secure a modern API by itself.

See exactly what the prefix changes

while(1);{"account":{"name":"Ada","balance":1250}}

The bytes are not valid JSON because JSON must begin with a valid JSON value. They are also hostile to naïve script execution because evaluation enters an infinite loop before reaching the object. A client that understands the convention strips the exact prefix and then parses the remaining JSON.

Understand the historical attack boundary

<script src="https://bank.example.test/private/transactions"></script>

Browsers have always permitted some cross-origin script inclusion. Historical attacks combined authenticated cookies, endpoints returning sensitive top-level arrays, and quirks that let an attacker observe values while another origin’s response was interpreted as JavaScript. The prefix made that particular execution path unusable.

Do not confuse JSON hijacking with CSRF

ProblemAttacker’s objectivePrincipal control
JSON hijacking/data inclusionRead sensitive response data cross-originCorrect response type, browser isolation and authorization
CSRFCause a victim’s browser to perform an unwanted state changeCSRF token/origin validation plus cookie policy
CORS misconfigurationMake browser expose a cross-origin response to attacker JavaScriptExplicit allowed-origin policy
XSSRun attacker script in the trusted originOutput encoding, sanitization, CSP and safe DOM APIs

while(1); does not stop a forged form submission, does not authorize an object, and does not contain an XSS payload already running on the trusted origin.

Return JSON with the correct representation headers

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
Cache-Control: private, no-store

{"account":{"name":"Ada","balance":1250}}

application/json declares the format. nosniff tells browsers not to reinterpret it through MIME sniffing. Sensitive user-specific responses should have an explicit cache policy; no-store is appropriate when persistence in browser or intermediary caches is unacceptable.

Authorize every object on every request

// Pseudocode
session = authenticate(request)
account = loadAccount(request.path.id)

if (!canRead(session.user, account)) {
    return 404
}
return json(account.safeView())

Same-origin browser rules are not an API authorization layer. Attackers can call the service directly, and an authenticated user may change an object identifier. Check the current principal’s permission on the requested object and serialize an allowlisted view.

Make credentialed CORS narrow and explicit

Access-Control-Allow-Origin: https://app.example.test
Access-Control-Allow-Credentials: true
Vary: Origin

The Fetch Standard does not allow Access-Control-Allow-Origin: * to share a response when credentials mode is include. Compare the request origin against an allowlist, emit that exact serialized origin only when allowed, and add Vary: Origin when caches may store variants.

Avoid reflecting arbitrary origins

// Dangerous
response.setHeader(
  "Access-Control-Allow-Origin",
  request.headers.origin
);
response.setHeader("Access-Control-Allow-Credentials", "true");

Blind reflection turns the attacker’s origin into an approved reader. Pattern matching also needs care: suffix checks such as endsWith("example.com") can accept notexample.com. Parse origins and compare scheme, host and port to known entries.

Keep ambient cookies constrained

Set-Cookie: session=opaque;
  Secure;
  HttpOnly;
  SameSite=Lax;
  Path=/

Secure restricts transport to HTTPS, HttpOnly blocks routine JavaScript access, and SameSite limits cross-site sending. Choose Strict, Lax, or the deliberately cross-site None; Secure from the actual login and embedding flow. Cookie attributes reduce exposure; the server must still authenticate and authorize.

Protect state-changing requests separately

POST /api/profile/email HTTP/1.1
Content-Type: application/json
X-CSRF-Token: session-bound-random-token
Origin: https://app.example.test

For cookie-authenticated state changes, require a strong CSRF control: a synchronizer token or a correctly implemented signed double-submit design, with Origin/Referer validation as defense in depth. Reject “simple” form content types when the endpoint contract requires JSON, but do not mistake a preflight for sole authorization.

Parse JSON; never evaluate it

const response = await fetch("/api/account", {
  credentials: "same-origin",
  headers: { Accept: "application/json" }
});
if (!response.ok) throw new Error("HTTP " + response.status);

const mediaType = response.headers.get("content-type") || "";
if (!mediaType.toLowerCase().startsWith("application/json")) {
  throw new Error("Unexpected response type");
}
const account = await response.json();

response.json() applies a JSON parser. Do not use eval, new Function, JSONP callbacks, or script tags to consume data. Those mechanisms collapse the boundary between data and executable code.

Handle a legacy prefixed service at one boundary

const prefix = "while(1);";
const text = await response.text();

if (!text.startsWith(prefix)) {
  throw new Error("Expected legacy JSON protection prefix");
}
const payload = JSON.parse(text.slice(prefix.length));

Strip only the exact documented prefix, in one adapter, after validating status and response type. Do not remove everything before the first brace; that can hide proxies returning HTML errors or attackers injecting unexpected bytes. Migrate the service to ordinary JSON when clients permit.

Prefer a top-level object for durable API evolution

{
  "items": [
    {"id": "txn_1", "amount": 25}
  ],
  "next_cursor": "opaque"
}

Wrapping a collection gives room for pagination and metadata and is recommended by OWASP as protection for older browser scenarios. It is defense in depth, not a reason to omit the modern controls around the response.

Keep sensitive JSON out of shared caches

Cache-Control: private, no-store
Pragma: no-cache
Vary: Origin, Cookie, Authorization

Choose directives from the deployment rather than copying this block blindly. An authenticated response cached without the correct key can leak between users even when CORS is perfect. Review CDN rules, reverse-proxy defaults, application caches and service workers.

Reduce the data before serializing it

safe = {
  "id": account.public_id,
  "display_name": account.display_name,
  "plan": account.plan_name
}

Do not serialize an ORM entity and delete a few obvious secrets afterward. Build an explicit response model. Exclude password hashes, reset tokens, internal permissions, fraud flags, private notes and fields the caller did not request or earn permission to see.

Add resource-isolation defense in depth

Cross-Origin-Resource-Policy: same-origin
Content-Security-Policy: default-src 'self'; object-src 'none'

Cross-Origin-Resource-Policy can ask supporting browsers to block cross-origin no-CORS use of the response. CSP primarily constrains resources loaded by documents, so apply it to the application pages as part of the broader architecture. Neither header replaces endpoint authorization.

Test the actual hostile origins

curl -i https://api.example.test/private/account   -H 'Origin: https://attacker.example'

curl -i https://api.example.test/private/account   -H 'Origin: https://app.example.test'   -H 'Cookie: session=TEST_SESSION'

Verify that unauthenticated calls fail, unauthorized object IDs fail, hostile origins receive no readable credentialed CORS grant, trusted origins get the intended headers, state changes require CSRF protection, and user-specific responses never enter a shared cache. Add these as integration tests, not a one-time browser check.

Use a layered acceptance checklist

  • Authentication and object-level authorization run before serialization.
  • The response is application/json with nosniff.
  • Credentialed CORS names only approved origins and varies caches by origin.
  • Session cookies use deliberate Secure, HttpOnly and SameSite settings.
  • State-changing routes have a separate CSRF defense.
  • Clients parse JSON as data and never use JSONP or evaluation.
  • Sensitive responses have an intentional cache and minimization policy.

The infinite-loop prefix solved a narrow historical execution problem. Modern private JSON is protected by an architecture: authorization, correct types, restricted cross-origin sharing, constrained credentials, safe parsing and cache discipline.

Sources: the solved Stack Overflow question (question by Jess; accepted answer by rjh, CC BY-SA), the WHATWG Fetch Standard’s credentialed CORS rules, MDN’s references for CORS, Content-Type and nosniff, and the OWASP AJAX Security Cheat Sheet.

Primary source: Review the official reference ↗