Cross-Site Script Inclusion (XSSI)
Also known as: XSSI, JSON hijacking, JSONP data leak, Script inclusion data leak
A web weakness in which an authenticated endpoint returns sensitive data as executable JavaScript or JSONP, so a script tag on another site can include it, ride the victim's cookies and expose the data to that site.
How it works
Cross-site script inclusion happens when an application serves sensitive data in a form a browser will run as a script. Browsers let any page include a script from any origin, and they send the user's cookies for that origin when they fetch it. If the response is valid JavaScript that carries personal or account data, the including page can end up reading it.
The flaw needs three conditions. The endpoint returns sensitive data for the signed-in user. The response is executable as a script, for example a JSONP call that wraps the data in a function, or a JavaScript file with data assigned to a global variable. And authentication relies only on ambient cookies, so the request from a foreign page is still treated as the user's. The response is not blocked by the same-origin policy because script inclusion is an allowed cross-origin load.
The defence is to stop serving data as script. Return data as JSON with a Content-Type of application/json and a nosniff header, and require something a foreign page cannot add, such as a custom request header or a bearer token, which also forces a CORS decision. An anti-XSSI prefix makes a JSON body unparseable as script, and JSONP should be retired in favour of CORS.
These controls are layered. The primary control is never to expose sensitive data through a script-includable response. The prefix and header checks catch what slips through, and a review checklist keeps new endpoints from repeating the pattern. Compare with CSRF, which forges actions, and CORS misconfiguration, which opens reads deliberately.
Walk through it
- 1Review the endpoint
- 2Name the safe format
- 3Add the anti-XSSI prefix
- Require more than a cookie
- Retire JSONP and verify the fix
A pre-release review flags the profile feed used by the rewards dashboard. It returns the signed-in member's details. Read the handler as a reviewer and decide what the browser will do with the response.
1// GET /api/profile.js?callback=render (cookie-authenticated)2router.get("/api/profile.js", requireSession, async (req, res) => {3 const profile = await db.members.get(req.session.user.id);4 const cb = req.query.callback ?? "render";5 res.type("application/javascript");6 res.send(`${cb}(${JSON.stringify(profile)});`);7});Spot it
- Authenticated endpoints returning personal or account data with a JavaScript content type, or with a callback query parameter.
- Script tags or script-initiated loads of your API paths from pages on other origins, visible in Referer or Origin data.
- Responses carrying sensitive fields that lack an application/json type, nosniff header or anti-XSSI prefix.
- Dynamic .js files that embed per-user values in a configuration review or scan.
- Requests to sensitive endpoints with Sec-Fetch-Dest: script from a cross-site context.
Application access log
ts=2026-10-11T10:02:11Z method=GET path=/api/profile.js query=callback=render user=u_7310 status=200 sec_fetch_dest=script sec_fetch_site=cross-site referer=https://partner-promo.example/offer
ts=2026-10-11T10:02:49Z method=GET path=/api/profile user=u_7310 status=200 sec_fetch_dest=empty sec_fetch_site=same-origin x_requested_with=presentWAF
ts=2026-10-11T10:02:11Z rule=cross-site-script-load action=log host=portal.contoso-rewards.example path=/api/profile.js dest=script site=cross-sitesplSplunk: sensitive endpoints loaded as scripts from another site
index=app sourcetype=access sec_fetch_dest=script sec_fetch_site=cross-site status=200
| stats count by path, user, refererPublic JavaScript libraries will appear; limit the search to authenticated API paths.
grepgrep: JSONP-style callbacks in access logs
grep -E 'path=/api/[^ ]* query=callback=' access.logStop it
Never serve sensitive data as executable script
Return data as JSON with Content-Type application/json and X-Content-Type-Options: nosniff, and retire JSONP and per-user dynamic script files. If the browser cannot run the body as code, a script tag from another site gains nothing.
Require something a cookie alone cannot supply
Authenticate API calls with a custom request header or a bearer token, and apply an explicit CORS policy. A script-tag load cannot add custom headers, so cookie-only access from a foreign page fails.
Add an anti-XSSI prefix and use token defences
Begin JSON bodies with a non-executable prefix such as )]}' that the first-party client strips, and protect state changes with anti-CSRF tokens. Treat both as defence in depth, not as a substitute for the first two principles.
Serve plain JSON and require a custom header
Vulnerable
router.get("/api/profile.js", requireSession, async (req, res) => {
const profile = await db.members.get(req.session.user.id);
res.type("application/javascript");
res.send(`${req.query.callback}(${JSON.stringify(profile)});`);
});Hardened
router.get("/api/profile", requireSession, requireHeader("x-requested-with"), async (req, res) => {
const profile = await db.members.get(req.session.user.id);
res.set("X-Content-Type-Options", "nosniff");
res.type("application/json");
res.send(")]}'\n" + JSON.stringify(profile));
});The first-party client removes the first line before JSON.parse. Use your framework's helpers where they exist.
Safe response headers
Hardened
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
Cache-Control: no-store- Remove JSONP endpoints; use CORS with an explicit allow-list instead.
- Serve API data as application/json with X-Content-Type-Options: nosniff.
- Require a custom header or bearer token on sensitive API calls rather than cookies alone.
- Do not embed per-user data in dynamically generated .js files.
- Prefix JSON bodies with an anti-XSSI string and strip it in the first-party client.
- Check Sec-Fetch-Dest and Sec-Fetch-Site on the server and reject cross-site script loads of sensitive paths.
If it already happened
Disable the JSONP or script-served endpoint, or change it to return plain JSON with a required header, and add a temporary block on cross-site script loads of the path.
Inventory every endpoint that returns user data with a script type or callback parameter, and migrate callers to the JSON and header design.
Assess which users' data was served to cross-site script loads using the access logs, notify as required, and confirm the fix with a test that includes the endpoint from a foreign page.
Add a review checklist item for every new data endpoint, ban JSONP in the framework template, and keep the Sec-Fetch log detection above.
Check yourself
1. Why can a page on another site read data from an authenticated endpoint that returns JSONP?
2. Which fix addresses the root cause of XSSI?
3. What is the purpose of an anti-XSSI prefix such as )]}' at the start of a JSON response?