Legitimate input Attack / malformed payload Blocked at this gate Passed through UNSAFE

Input Validation Pipeline (2D)

This 2D companion drives the same defense-in-depth idea as the 3D pipeline through a plain canvas view built for reading the validation logic rather than orbiting a scene. Real SQL-injection fragments, XSS fragments, oversized strings and null-byte tricks are drawn from an actual payload pool and flow left to right through four genuinely evaluating stages — a length check, an allow-list charset check, a character-class/keyword scan, and an encoding-normalization check that re-validates the payload after percent-decoding it. Toggle any stage off, drag the max-length threshold or the malicious-injection-rate slider, and watch the live per-stage catch-rate breakdown change in real time.

', note: 'XSS script tag' }, { s: '', note: 'XSS via onerror' }, { s: 'javascript:alert(1)', note: 'XSS javascript: URI' }, { s: 'SELECT password FROM admin_users', note: 'SQL keywords, clean charset' }, { s: "' UNION SELECT NULL--", note: 'SQL UNION injection' }, { s: '../../../etc/passwd', note: 'path traversal' }, { s: new Array(121).join('A'), note: 'oversized payload (DoS)' }, { s: 'report%00.exe.jpg', note: 'null-byte extension bypass' }, { s: '%3Cb%3Estyled%3Cb%3E', note: 'double-encoded tag injection' }, { s: 'username', note: 'embedded null byte' } ]; /* ===================== the four real validation stages ===================== */ var ALLOW_RE = /^[\p{L}\p{N} .,'_@!?%\-]*$/u; var DANGEROUS_RE = /(--|;|<|>|=|`|\|\||&&|\$\(|\bSELECT\b|\bUNION\b|\bDROP\b|\bINSERT\b|\bDELETE\b|\bFROM\b|\bscript\b|\bjavascript\b|\bonerror\b|\bonload\b)/i; function checkLength(s, maxLen) { return s.length <= maxLen; } function checkAllow(s) { return ALLOW_RE.test(s); } function checkCharClass(s) { return !DANGEROUS_RE.test(s); } function checkEncoding(s) { if (s.indexOf('') !== -1 || /%00/i.test(s)) return false; var decoded = s; try { decoded = decodeURIComponent(s.replace(/\+/g, ' ')); } catch (e) { decoded = s; } if (decoded === s) return true; // nothing new to normalize return ALLOW_RE.test(decoded) && !DANGEROUS_RE.test(decoded); } var STAGES = [ { key: 'length', label: 'length', check: function (s, p) { return checkLength(s, p.maxLen); } }, { key: 'allow', label: 'allow-list', check: function (s) { return checkAllow(s); } }, { key: 'charclass', label: 'char-class', check: function (s) { return checkCharClass(s); } }, { key: 'encode', label: 'encoding', check: function (s) { return checkEncoding(s); } } ]; var stageEnabled = { length: true, allow: true, charclass: true, encode: true }; document.querySelectorAll('.stage-toggle').forEach(function (el) { el.addEventListener('click', function () { var key = el.dataset.stage; stageEnabled[key] = !stageEnabled[key]; el.classList.toggle('on', stageEnabled[key]); }); }); /* ===================== parameters ===================== */ var maxLen = 64, malRate = 0.4, spawnInterval = 0.9; function readParams() { maxLen = Number(maxLenSlider.value); malRate = Number(malRateSlider.value) / 100; spawnInterval = Number(rateSlider.value); vMaxLen.textContent = maxLen + ' chars'; vMalRate.textContent = Math.round(malRate * 100) + '%'; vRate.textContent = '1 / ' + spawnInterval.toFixed(1) + 's'; } maxLenSlider.addEventListener('input', readParams); malRateSlider.addEventListener('input', readParams); rateSlider.addEventListener('input', readParams); readParams(); /* ===================== counters ===================== */ var safeCount = 0, blockedCount = 0, unsafeCount = 0, totalCount = 0, attackTotal = 0, attackCaught = 0; var stageCatch = { length: 0, allow: 0, charclass: 0, encode: 0 }; function refreshStats() { statSafe.textContent = String(safeCount); statBlocked.textContent = String(blockedCount); statUnsafe.textContent = String(unsafeCount); statTotal.textContent = String(totalCount); statBlockRate.textContent = totalCount ? Math.round((blockedCount / totalCount) * 100) + '%' : '0%'; statCatchRate.textContent = attackTotal ? Math.round((attackCaught / attackTotal) * 100) + '%' : '0%'; stageBreakdownEl.textContent = 'Caught by: length ' + stageCatch.length + ' · allow-list ' + stageCatch.allow + ' · char-class ' + stageCatch.charclass + ' · encoding ' + stageCatch.encode; } refreshStats(); var paused = false; btnPause.addEventListener('click', function () { paused = !paused; btnPause.textContent = paused ? 'Resume' : 'Pause'; }); btnClear.addEventListener('click', function () { safeCount = 0; blockedCount = 0; unsafeCount = 0; totalCount = 0; attackTotal = 0; attackCaught = 0; stageCatch = { length: 0, allow: 0, charclass: 0, encode: 0 }; refreshStats(); }); /* ===================== packets ===================== */ var LANE_COUNT = 6; var packets = []; // {str, isAttack, note, lane, x, stageIdx, blocked, blockedStage, done, unsafe, deathT} var nextLane = 0; function spawnPacket() { var isAttack = Math.random() < malRate; var str, note; if (isAttack) { var a = ATTACK_PAYLOADS[Math.floor(Math.random() * ATTACK_PAYLOADS.length)]; str = a.s; note = a.note; } else { str = LEGIT_PAYLOADS[Math.floor(Math.random() * LEGIT_PAYLOADS.length)]; note = 'legitimate'; } packets.push({ str: str, note: note, isAttack: isAttack, lane: nextLane % LANE_COUNT, x: 0, stageIdx: 0, blocked: false, blockedStage: -1, done: false, unsafe: false, deathT: 0 }); nextLane++; } var GATE_FRAC = [0.20, 0.42, 0.64, 0.86]; // fraction of pipeline width per gate var SPEED_FRAC_PER_SEC = 0.16; // fraction of pipeline width travelled per second function evaluateGate(p, stageIndex) { var stage = STAGES[stageIndex]; if (!stageEnabled[stage.key]) return; // stage off: nothing happens var passes = stage.check(p.str, { maxLen: maxLen }); if (!passes) { p.blocked = true; p.done = true; p.deathT = 0; p.blockedStage = stageIndex; blockedCount++; totalCount++; stageCatch[stage.key]++; if (p.isAttack) { attackCaught++; } refreshStats(); } } function finishPacket(p) { p.done = true; if (p.isAttack) { unsafeCount++; p.unsafe = true; } else { safeCount++; } totalCount++; refreshStats(); } var spawnTimer = 0; function updatePackets(dt) { for (var i = 0; i < packets.length; i++) { var p = packets[i]; if (p.blocked) { p.deathT += dt; continue; } if (p.done) continue; p.x += SPEED_FRAC_PER_SEC * dt; for (var g = p.stageIdx; g < GATE_FRAC.length; g++) { if (p.x >= GATE_FRAC[g]) { p.stageIdx = g + 1; evaluateGate(p, g); if (p.blocked) break; } else break; } if (!p.blocked && p.x >= 1) finishPacket(p); } for (var j = packets.length - 1; j >= 0; j--) { var pk = packets[j]; if (pk.blocked && pk.deathT > 0.7) packets.splice(j, 1); else if (pk.done && !pk.blocked && pk.x > 1.06) packets.splice(j, 1); } } /* ===================== canvas sizing: ResizeObserver only ===================== */ var cssW = 0, cssH = 0, dpr = Math.min(window.devicePixelRatio || 1, 2); var sized = false; function resizeCanvas() { var w = Math.max(1, Math.floor(wrap.clientWidth)); var h = Math.max(1, Math.floor(wrap.clientHeight || wrap.clientWidth / 1.6)); cssW = w; cssH = h; canvas.width = Math.max(1, Math.floor(w * dpr)); canvas.height = Math.max(1, Math.floor(h * dpr)); canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; sized = true; } var ro = new ResizeObserver(function () { resizeCanvas(); }); ro.observe(wrap); /* ===================== drawing ===================== */ function truncate(s, n) { var vis = s.replace(/[-]/g, '␣'); return vis.length > n ? vis.slice(0, n - 1) + '…' : vis; } function draw() { if (!sized || cssW <= 1 || cssH <= 1) return; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, cssW, cssH); ctx.fillStyle = '#0a0507'; ctx.fillRect(0, 0, cssW, cssH); var padX = cssW * 0.03, pipeW = cssW * 0.94; var laneH = Math.max(24, (cssH - 20) / LANE_COUNT); // gates for (var g = 0; g < GATE_FRAC.length; g++) { var gx = padX + GATE_FRAC[g] * pipeW; ctx.strokeStyle = 'rgba(255,82,82,' + (stageEnabled[STAGES[g].key] ? 0.45 : 0.12) + ')'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(gx, 4); ctx.lineTo(gx, cssH - 4); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = stageEnabled[STAGES[g].key] ? '#ff8a80' : 'rgba(255,255,255,0.3)'; ctx.font = '600 ' + Math.max(9, Math.min(11, cssW * 0.018)) + 'px JetBrains Mono, monospace'; ctx.textAlign = 'center'; ctx.fillText(STAGES[g].label.toUpperCase(), gx, 16); } ctx.textAlign = 'left'; // packets for (var i = 0; i < packets.length; i++) { var p = packets[i]; var px = padX + Math.min(p.x, 1.05) * pipeW; var py = 22 + p.lane * laneH + laneH / 2; var color = p.isAttack ? '#ff5252' : '#29e0c8'; var r = Math.max(4, Math.min(7, cssW * 0.008)); var alpha = 1; if (p.blocked) { var t = Math.min(1, p.deathT / 0.7); py += t * laneH * 0.9; alpha = 1 - t; color = '#ffca28'; } else if (p.unsafe) { color = '#ff8a65'; } ctx.globalAlpha = alpha; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.fill(); ctx.font = Math.max(8, Math.min(10, cssW * 0.014)) + 'px JetBrains Mono, monospace'; ctx.fillStyle = 'rgba(207,216,220,' + (0.75 * alpha) + ')'; ctx.fillText(truncate(p.str, 22), px + r + 5, py + 3); ctx.globalAlpha = 1; } } /* animation loop */ var lastT = null; function animate(t) { requestAnimationFrame(animate); if (lastT === null) lastT = t; var dt = (t - lastT) / 1000; lastT = t; if (dt > 0.1) dt = 0.1; if (!paused) { spawnTimer -= dt; if (spawnTimer <= 0) { spawnPacket(); spawnTimer = spawnInterval; } updatePackets(dt); } draw(); } requestAnimationFrame(animate); })();