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.
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.
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.
# 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.
JSONP manifests in several forms beyond the obvious ?callback= parameter. A scanner that looks only for that parameter name misses the others silently.
| Variant | Trigger | Confirming signal |
|---|---|---|
| Classic callback echo | ?callback=, ?jsonp=, ?cb= | User data wrapped in the nonce function name, Content-Type: text/javascript |
| Callback without auth gate | Endpoint is public but returns user context from the session cookie | Response differs between authenticated and unauthenticated probes |
| Unicode / escape bypass | Callback parameter with Unicode-escaped angle brackets | Response contains unescaped characters exploitable inside a script context |
| Flash / legacy crossdomain.xml | Permissive 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.
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.
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.