Engineering · Sep 27, 2026 · 6 min read

JSONP hijacking: when the padding becomes a data leak

A JSONP endpoint wraps authenticated data in a caller-controlled callback - any page the victim visits can read the response through a script tag, with no CORS restriction.

JSONP was a pre-CORS workaround for the same-origin policy: the server wraps a JSON payload in a caller-controlled function name, and the browser executes the result as a script. The callback is attacker-controlled. Any page on the internet can include a script tag pointing at your authenticated API, read the response through the callback, and exfiltrate the data - no user interaction required beyond a page visit.

01The mechanism: a script tag becomes a cross-origin read

When a server returns Content-Type: text/javascript and a body of myCallback({"email":"alice@example.com"}), the browser has no choice: it calls myCallback with the data as a plain JavaScript object. The attacker controls the callback name via a query parameter, so any page they host can define that function, include the script tag, and intercept every field in the response. Same-origin policy does not apply to script tags - it was never designed to.

Victim browser, logged into api.example.com visits attacker page ──► attacker.io/steal.html steal.html injects: <script>function cb(d){exfil(d.email)}</script> <script src="https://api.example.com/profile?callback=cb"> api.example.com returns (with the victim's session cookie): cb({"email":"alice@example.com","phone":"+1 555 0191"}) Browser executes the script ──► cb() fires, data lands in exfil()
A single script tag turns an authenticated JSONP endpoint into a cross-origin read for any attacker page the victim visits.

02Confirming the leak with a two-session differential

The confirming probe mirrors the attack without touching any real user data. Two sessions are needed: one holds a valid authenticated session, the second is unauthenticated. Request the endpoint with a nonce callback name from each session and compare the responses. If the authenticated response wraps user-specific data in the callback and the unauthenticated response does not - or returns an error - the endpoint leaks data to any cross-origin caller that can trigger the same authenticated request.

jsonp-probe.httphttp
# authenticated session - probe callback name is a nonce marker
GET /api/profile?callback=apNonce_62615533
Cookie: session=<valid-token>

HTTP/1.1 200 OK
Content-Type: text/javascript

apNonce_62615533({"email":"alice@example.com","role":"admin"})   # confirming artifact: user data in the callback

# unauthenticated probe - same endpoint, no session cookie
GET /api/profile?callback=apNonce_62615533

HTTP/1.1 401 Unauthorized   # differential confirmed: data only flows with session

The nonce callback name proves the server echoes the parameter without sanitization. No real exfiltration happens: the probe reads only the response it itself triggered, and the data appears only in the scanner's evidence log - not in any third-party request. Sensitive field values in the confirming response are masked before storage.

Proven - HighJSONP data exposure - authenticated profile endpoint - /api/profile
GET /api/profile?callback=apNonce_62615533 (authenticated session) ──► 200 OK Content-Type: text/javascript apNonce_62615533({"email":"[masked]","role":"admin"}) unauthenticated probe ──► 401 - differential confirms session dependency
Oracle: differential two-session. Callback echo unescaped. Severity high - any authenticated cross-origin page visit leaks the profile payload. Escalated to critical when the payload includes session tokens, payment data or PII with regulatory scope.

03Variants that a single-pattern check misses

JSONP manifests in several forms beyond the obvious ?callback= parameter. A scanner that looks only for that parameter name misses the others silently.

VariantTriggerConfirming signal
Classic callback echo?callback=, ?jsonp=, ?cb=User data wrapped in the nonce function name, Content-Type: text/javascript
Callback without auth gateEndpoint is public but returns user context from the session cookieResponse differs between authenticated and unauthenticated probes
Unicode / escape bypassCallback parameter with Unicode-escaped angle bracketsResponse contains unescaped characters exploitable inside a script context
Flash / legacy crossdomain.xmlPermissive allow-access-from domain="*"Any Flash or PDF origin allowed; policy file returned with wildcard

The callback name echoing behavior also creates a reflected XSS path when the response Content-Type is set to text/html rather than text/javascript. The probe checks the content type in every response: a JSONP endpoint that misidentifies its own content type upgrades the finding from data exposure to a reflected script execution context.

04Severity, scope, and the fix

Severity follows the data class in the authenticated response. A callback that wraps a user's email and role is high - unauthorized cross-origin reads of personal data and session context. A callback that includes a session token or payment card data is critical with a 7-day SLA - arbitrary session hijack or PCI-in-scope data exposure over a zero-click mechanism. A callback that wraps only a non-sensitive public identifier is low - data class, not the mechanism, anchors the number.

0
user interaction required by the attacker
4
common callback parameter names probed
2
sessions in the confirming differential

The fix has two steps: remove the JSONP endpoint entirely and replace it with a CORS-enabled JSON response restricted to an explicit origin allowlist. If the endpoint must serve a legacy consumer that cannot be updated, at a minimum enforce CSRF token validation on the session cookie and strip the ?callback= parameter from any endpoint that touches authenticated state. Never set Content-Type: text/html on a JSONP response - that combination makes callback sanitization a requirement for every character class, which is a losing game. The right fix is CORS plus JSON, not a callback denylist.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →