JSONP Callback XSS: Turning Legacy API Responses Into Script Execution
JSONP predates modern CORS. A page loads an API endpoint through a <script src>, and the server wraps JSON in a caller-chosen function:
showProfile({"name":"Essam"});
That caller-chosen name is the danger. If callback accepts JavaScript syntax rather than a safe identifier, the response can become executable attacker input on the API's origin.
Find the old plumbing
Search proxy history and JavaScript bundles for callback=, jsonp=, cb=, or jQuery's callback=?. Legacy analytics, maps, stock widgets, and public profile APIs keep JSONP alive long after the main app moves to fetch.
Start with harmless substitutions:
callback=test123should returntest123({...})callback=a.bchecks whether dotted names are allowed- URL-encoded punctuation reveals validation boundaries
- Missing callback may return raw JSON or a default wrapper
Confirm Content-Type. application/javascript is expected for JSONP, but browsers execute a script response regardless of many loose MIME choices unless nosniff and destination rules intervene.
The injection test
A weak endpoint may concatenate the parameter directly:
?callback=alert(document.domain)//
The response becomes alert(document.domain)//({...}). Other parsers require balancing parentheses or comments. Use a non-destructive marker first, such as setting document.title, and encode characters once. Double-decoding at a gateway can create a bypass that looks impossible in Repeater's decoded view.
Some endpoints whitelist letters, digits, underscore, dollar sign, and dots. That usually blocks direct XSS. Do not report “callback is reflected” when only valid function names survive. JSONP is intentionally executable.
Where does code execute?
If you open the API URL directly and see alert(1), you have script execution in a top-level document only if the browser treats it as active content. The stronger proof embeds the endpoint:
<script src="https://api.target.example/data?callback=PAYLOAD"></script>
Now the JavaScript executes in the embedding page's origin, not automatically in api.target.example. That distinction changes impact. For arbitrary sites, intended JSONP already lets them read the public response.
The high-value case is when a trusted target page includes an attacker-influenced JSONP URL, or the JSONP endpoint shares cookies and can be navigated into a useful first-party context. Another angle is CSP: a policy may allow scripts from api.target.example; an injectable callback there becomes a CSP bypass gadget for a separate HTML injection.
Test data exposure separately
JSONP endpoints sometimes return private data using session cookies. Because any origin can include the script, an attacker can define the callback and steal the response. Modern SameSite cookies may block that cross-site request, but same-site sibling origins can still matter. Use your own account and show one private canary field.
Keep two findings clear: callback injection is XSS/gadget behavior; authenticated JSONP data theft is cross-origin disclosure. They may chain, but the evidence differs.
Report the real boundary
Attach the raw response, standalone HTML PoC, execution origin, cookie behavior, and CSP relationship. Recommend removing JSONP in favor of CORS with an explicit origin allowlist. If JSONP must remain, validate callback names against a strict identifier grammar, return only public data, and send defensive content headers.
OWASP's cross-site scripting overview covers reflected execution fundamentals. For JSONP, the deciding question is simple: did you escape the intended function-name grammar, or did the endpoint behave exactly as designed?