forked from balance3840/sql-injection-demo
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
494 lines (443 loc) · 26.1 KB
/
index.html
File metadata and controls
494 lines (443 loc) · 26.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQL Injection — Live Demo</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'db-bg': '#0f0f0f',
'db-surface': '#161616',
'db-s2': '#1e1e1e',
'db-line': '#2a2a2a',
'db-accent': '#e8ff5a',
'db-green': '#5affb8',
'db-muted': '#555555',
'db-text': '#d4d4d4',
'db-hi': '#f0f0f0',
'db-red': '#ff6b6b',
'db-blue': '#60a5fa',
'db-purple': '#c792ea',
},
fontFamily: {
mono: ['"IBM Plex Mono"', 'monospace'],
sans: ['"IBM Plex Sans"', 'sans-serif'],
}
}
}
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style>
/* ── Non-Tailwind bits ── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 3px; }
/* SQL syntax colours — used by innerHTML, can't be Tailwind */
.sql-kw { color: #c792ea; }
.sql-str { color: #e8ff5a; }
.sql-inject { color: #ff6b6b; font-weight: 500; text-decoration: underline wavy #ff6b6b; }
.sql-param { color: #5affb8; font-weight: 500; }
/* Results table — needs :hover and sticky header */
.rtable { width: 100%; border-collapse: collapse; font-family: 'IBM Plex Mono', monospace; font-size: 12px; }
.rtable thead th { padding: 7px 12px; text-align: left; font-weight: 500; color: #e8ff5a; font-size: 11px; background: #161616; border-bottom: 1px solid #2a2a2a; white-space: nowrap; position: sticky; top: 0; }
.rtable tbody tr { border-bottom: 1px solid rgba(255,255,255,.03); }
.rtable tbody tr.highlight { background: rgba(255,107,107,.07); }
.rtable tbody td { padding: 6px 12px; color: #d4d4d4; white-space: nowrap; }
/* rgba border/bg combos — hard to do in Tailwind without arbitrary values */
.danger-box { background: rgba(255,107,107,.06); border: 1px solid rgba(255,107,107,.2); color: #ff6b6b; }
.safe-box { background: rgba(90,255,184,.06); border: 1px solid rgba(90,255,184,.2); color: #5affb8; }
.neutral-box{ background: #1e1e1e; border: 1px solid #2a2a2a; color: #d4d4d4; }
.badge-danger { background: rgba(255,107,107,.15); color: #ff6b6b; }
.badge-safe { background: rgba(90,255,184,.12); color: #5affb8; }
.badge-empty { background: #161616; color: #555; }
.sql-box { background: #0f0f0f; border: 1px solid #2a2a2a; transition: border-color .2s; }
.sql-box.danger { border-color: #ff6b6b; background: rgba(255,107,107,.04); }
.sql-box.safe-q { border-color: #5affb8; background: rgba(90,255,184,.03); }
.chip:hover { border-color: #ff6b6b; color: #ff6b6b; background: rgba(255,107,107,.06); }
.mode-btn.active.vuln { background: rgba(255,107,107,.15); color: #ff6b6b; }
.mode-btn.active.safe { background: rgba(90,255,184,.12); color: #5affb8; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/sql-wasm.js"></script>
</head>
<body class="bg-db-bg text-db-text font-sans text-sm leading-relaxed min-h-screen">
<!-- ══ HEADER ══ -->
<header class="bg-db-surface border-b border-db-line px-8 py-3.5 flex items-center gap-3">
<span class="font-mono text-sm font-medium text-db-red tracking-[0.05em]">sql.injection</span>
<span class="text-db-muted">/</span>
<span class="text-[13px] text-db-muted">interactive demo — Week 2</span>
<div class="ml-auto flex border border-db-line rounded overflow-hidden">
<button class="mode-btn active vuln font-mono text-[11px] font-medium tracking-[0.05em] px-3.5 py-1.5 bg-db-s2 text-db-muted border-none cursor-pointer transition-all"
id="btn-vuln" onclick="setMode('vuln')">⚠ Vulnerable</button>
<button class="mode-btn font-mono text-[11px] font-medium tracking-[0.05em] px-3.5 py-1.5 bg-db-s2 text-db-muted border-none cursor-pointer transition-all"
id="btn-safe" onclick="setMode('safe')">✓ Protected</button>
</div>
</header>
<!-- ══ LAYOUT ══ -->
<div class="grid grid-cols-2 min-h-[calc(100vh-52px)]">
<!-- LEFT: the "app" -->
<div class="border-r border-db-line p-7 flex flex-col gap-5">
<div class="font-mono text-[11px] font-medium tracking-[0.12em] uppercase pb-3 border-b border-db-line text-db-red"
id="panel-mode-label">⚠ Vulnerable version</div>
<!-- App mockup -->
<div>
<div class="font-mono text-[10px] text-db-muted tracking-[0.1em] uppercase mb-1.5">The app</div>
<div class="bg-db-surface border border-db-line rounded-md overflow-hidden">
<!-- macOS-style bar -->
<div class="bg-db-s2 border-b border-db-line px-4 py-2.5 flex items-center gap-2 font-mono text-[13px] text-db-muted">
<div class="w-2 h-2 rounded-full bg-[#ff5f57]"></div>
<div class="w-2 h-2 rounded-full bg-[#febc2e]"></div>
<div class="w-2 h-2 rounded-full bg-[#28c840]"></div>
<span class="ml-1">TaskManager — Search</span>
</div>
<!-- App content -->
<div class="p-5">
<div class="text-base font-medium text-db-hi mb-1">Search tasks by user</div>
<div class="text-xs text-db-muted mb-4">Type a name to see their assigned tasks</div>
<div class="font-mono text-xs text-db-muted mb-1.5">Username</div>
<div class="flex gap-2">
<input type="text" id="search-input" placeholder="e.g. Alice Jensen" oninput="onInput()"
class="flex-1 bg-db-bg border border-db-line rounded-sm text-db-hi font-mono text-[13px] px-3 py-2 outline-none transition-colors focus:border-db-blue" />
<button onclick="runSearch()"
class="font-mono text-xs font-medium px-4 py-2 rounded-sm bg-db-blue text-black cursor-pointer hover:opacity-85 transition-opacity border-none">
Search
</button>
</div>
</div>
</div>
</div>
<!-- Attack chips -->
<div id="attack-section">
<div class="font-mono text-[11px] text-db-muted mb-1.5">💡 Try these attack strings:</div>
<div class="flex flex-wrap gap-1.5">
<div class="chip font-mono text-[11px] px-2.5 py-1 rounded-sm border border-db-line bg-db-s2 text-db-muted cursor-pointer transition-all"
onclick="tryAttack(this.dataset.val)" data-val="' OR '1'='1">dump all users</div>
<div class="chip font-mono text-[11px] px-2.5 py-1 rounded-sm border border-db-line bg-db-s2 text-db-muted cursor-pointer transition-all"
onclick="tryAttack(this.dataset.val)" data-val="Alice' --">bypass filter</div>
<div class="chip font-mono text-[11px] px-2.5 py-1 rounded-sm border border-db-line bg-db-s2 text-db-muted cursor-pointer transition-all"
onclick="tryAttack(this.dataset.val)" data-val="'; DELETE FROM task; --">delete all tasks</div>
<div class="chip font-mono text-[11px] px-2.5 py-1 rounded-sm border border-db-line bg-db-s2 text-db-muted cursor-pointer transition-all"
onclick="tryAttack(this.dataset.val)" data-val="'; DROP TABLE user; --">drop table</div>
<div class="chip font-mono text-[11px] px-2.5 py-1 rounded-sm border border-db-line bg-db-s2 text-db-muted cursor-pointer transition-all"
onclick="tryAttack(this.dataset.val)" data-val="' UNION SELECT id,email,name,NULL,NULL FROM user --">extract emails</div>
</div>
</div>
<!-- SQL box -->
<div>
<div class="font-mono text-[10px] text-db-muted tracking-[0.1em] uppercase mb-1.5">Query being executed</div>
<div class="font-mono text-[10px] text-db-muted tracking-[0.08em] uppercase mb-1">Generated SQL</div>
<div class="sql-box rounded px-3.5 py-3 font-mono text-[12.5px] leading-[1.7] whitespace-pre-wrap break-all min-h-[52px]"
id="sql-display">
<span class="sql-kw">SELECT</span> * <span class="sql-kw">FROM</span> task <span class="sql-kw">WHERE</span> user_id = (<span class="sql-kw">SELECT</span> id <span class="sql-kw">FROM</span> user <span class="sql-kw">WHERE</span> name = '<span class="sql-str">…</span>')
</div>
</div>
<!-- Results -->
<div>
<div class="font-mono text-[10px] text-db-muted tracking-[0.1em] uppercase mb-1.5">Result</div>
<div class="border border-db-line rounded overflow-hidden">
<div class="bg-db-s2 border-b border-db-line px-3.5 py-1.5 font-mono text-[11px] text-db-muted flex items-center gap-2">
<span>task</span>
<span class="badge badge-empty px-2 py-0.5 rounded-sm text-[10px] font-medium" id="result-badge">no query run</span>
</div>
<div id="results-body">
<div class="text-center font-mono text-xs text-db-muted py-4">Run a search to see results</div>
</div>
</div>
</div>
<!-- Explain area -->
<div id="explain-area">
<div class="neutral-box rounded px-3.5 py-3 text-[13px] leading-relaxed">
Type a name above, then try one of the attack strings to see what happens.
</div>
</div>
</div>
<!-- RIGHT: code explanation -->
<div class="p-7 flex flex-col gap-5">
<div class="font-mono text-[11px] font-medium tracking-[0.12em] uppercase pb-3 border-b border-db-line text-db-green"
id="code-panel-label">The code — what's happening</div>
<!-- Code display -->
<div>
<div class="font-mono text-[10px] text-db-muted tracking-[0.1em] uppercase mb-1.5">Backend function</div>
<pre id="code-display"
class="bg-db-surface border border-db-line rounded px-4 py-3.5 font-mono text-[12.5px] leading-[1.8] overflow-x-auto text-db-text whitespace-pre"></pre>
</div>
<!-- Analysis -->
<div id="analysis-area">
<div class="font-mono text-[10px] text-db-muted tracking-[0.1em] uppercase mb-1.5">Analysis</div>
<div class="neutral-box rounded px-3.5 py-3 text-[13px] leading-relaxed" id="analysis-box">
Select an attack string on the left to see the analysis.
</div>
</div>
<!-- Real-world risk -->
<div>
<div class="font-mono text-[10px] text-db-muted tracking-[0.1em] uppercase mb-1.5">The real-world risk</div>
<div class="text-[13px] leading-[1.7] text-db-text flex flex-col gap-2.5">
<p>The app in this demo is the <strong class="text-db-hi">task management system from your assignment</strong>.
A manager types a name into a search box to see someone's tasks. Normal inputs like
<code class="bg-db-s2 px-1 py-0.5 rounded-sm font-mono text-xs">Alice Jensen</code> work perfectly.</p>
<p>But the backend builds the SQL query by concatenating the input directly into a string.
This means an attacker doesn't need an account, doesn't need to know the schema, and doesn't
need any special tools — just a text box and some knowledge of SQL syntax.</p>
<p class="text-db-muted text-xs">SQL injection has been in the <strong class="text-db-text">OWASP Top 10</strong>
most critical web vulnerabilities for over 20 years. It is responsible for some of the largest
data breaches in history — not because it is sophisticated, but because it is easy to overlook
when everything works fine in testing.</p>
</div>
</div>
<!-- Fix hint -->
<div id="fix-hint" style="display:none">
<div class="font-mono text-[10px] text-db-muted tracking-[0.1em] uppercase mb-1.5">Your task</div>
<div class="neutral-box rounded px-3.5 py-3 text-[13px] leading-relaxed">
Switch to <strong class="text-db-green">Protected mode</strong> above to see how the same query
can be written safely. Then for your assignment: research what this technique is called,
understand why it works, and rewrite the function in your
<code class="font-mono text-xs">.sql</code> file.
</div>
</div>
</div>
</div>
<!-- ══ RESPONSIVE ══ -->
<style>
@media (max-width: 900px) {
.grid-cols-2 { grid-template-columns: 1fr; }
.border-r { border-right: none; border-bottom: 1px solid #2a2a2a; }
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/sql-wasm.js"></script>
<script>
let db = null, mode = 'vuln', lastInput = '';
const SEED = `
CREATE TABLE user(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL);
CREATE TABLE status(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL);
CREATE TABLE task(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, user_id INTEGER, status_id INTEGER, due_date TEXT);
INSERT INTO user(name,email) VALUES
('Alice Jensen','alice@company.com'),
('Bob Madsen','bob@company.com'),
('Clara Thomsen','clara@company.com'),
('David Eriksen','david@company.com'),
('Eva Nielsen','eva@company.com');
INSERT INTO status(name) VALUES('To Do'),('In Progress'),('Done');
INSERT INTO task(title,user_id,status_id,due_date) VALUES
('Fix login bug',1,2,'2026-04-01'),
('Write API docs',1,1,'2026-04-10'),
('Refactor user service',1,1,'2026-04-15'),
('Design login page',4,3,'2026-03-20'),
('Set up CI pipeline',3,3,'2026-03-15'),
('Deploy to staging',3,2,'2026-04-05'),
('Unit tests for auth',5,1,'2026-04-20'),
('Performance testing',5,1,'2026-04-25'),
('Mobile responsive fixes',2,2,'2026-03-28'),
('Write onboarding guide',2,1,'2026-04-08');
`;
const VULN_FN = `function getTasksByUser(userName) {
// ⚠ User input glued directly into SQL string
const query = \`SELECT * FROM task
WHERE user_id = (
SELECT id FROM user
WHERE name = '\${userName}'
)\`;
db.all(query, (err, rows) => {
displayResults(rows);
});
}`;
const SAFE_FN = `function getTasksByUser(userName) {
// ✓ Input passed separately — never touches the SQL string
const query = \`SELECT * FROM task
WHERE user_id = (
SELECT id FROM user
WHERE name = ?
)\`;
db.all(query, [userName], (err, rows) => {
displayResults(rows);
});
}`;
const ATTACKS = {
"' OR '1'='1": {
title: "Dump all records",
what: "The injected OR '1'='1 makes the WHERE clause always true. Instead of filtering by name, it returns every task in the database — exposing data from all users.",
how: "The single quote closes the name string early. The rest becomes valid SQL that the database evaluates normally.",
severity: "high"
},
"Alice' --": {
title: "Bypass filter with comment",
what: "The -- starts a SQL comment, causing the database to ignore everything after it — including the closing quote. The query runs as if the WHERE clause ended after Alice'.",
how: "Comment injection is often used to bypass authentication checks or strip out additional conditions.",
severity: "medium"
},
"'; DELETE FROM task; --": {
title: "Delete all tasks",
what: "A second SQL statement is injected after the semicolon. The database executes both: the original SELECT and then a DELETE that wipes every task row.",
how: "Whether this works depends on whether the library allows multiple statements. Many do — including the sqlite3 Node.js package in its default configuration.",
severity: "critical"
},
"'; DROP TABLE user; --": {
title: "Destroy the user table",
what: "Same technique as DELETE but more destructive — drops the entire user table including its schema. The application immediately breaks for every user.",
how: "In production systems without backups, this is unrecoverable. With backups it still means downtime and investigation.",
severity: "critical"
},
"' UNION SELECT id,email,name,NULL,NULL FROM user --": {
title: "Steal all user emails",
what: "A UNION attack appends a second SELECT to the original query, pulling every row from the user table — including emails — mixed into the task results.",
how: "The attacker needs the same number of columns as the original query and compatible types. A few trials is usually enough to figure this out.",
severity: "high"
}
};
async function init() {
const SQL = await initSqlJs({ locateFile: f => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/${f}` });
db = new SQL.Database();
db.run(SEED);
updateCodePanel();
}
function setMode(m) {
mode = m;
document.getElementById('btn-vuln').className =
'mode-btn' + (m === 'vuln' ? ' active vuln' : '') +
' font-mono text-[11px] font-medium tracking-[0.05em] px-3.5 py-1.5 bg-db-s2 text-db-muted border-none cursor-pointer transition-all';
document.getElementById('btn-safe').className =
'mode-btn' + (m === 'safe' ? ' active safe' : '') +
' font-mono text-[11px] font-medium tracking-[0.05em] px-3.5 py-1.5 bg-db-s2 text-db-muted border-none cursor-pointer transition-all';
const modeLabel = document.getElementById('panel-mode-label');
modeLabel.textContent = m === 'vuln' ? '⚠ Vulnerable version' : '✓ Protected version';
modeLabel.className = 'font-mono text-[11px] font-medium tracking-[0.12em] uppercase pb-3 border-b border-db-line ' + (m === 'vuln' ? 'text-db-red' : 'text-db-green');
document.getElementById('attack-section').style.display = m === 'vuln' ? 'block' : 'none';
updateCodePanel();
if (lastInput) runSearch();
else updateSQLDisplay(document.getElementById('search-input').value);
}
function updateCodePanel() {
const el = document.getElementById('code-display');
el.innerHTML = mode === 'vuln'
? VULN_FN
.replace('⚠', '<span style="color:#ff6b6b">⚠</span>')
.replace(/(\$\{userName\})/g, '<span style="color:#ff6b6b;text-decoration:underline wavy #ff6b6b">$1</span>')
: SAFE_FN
.replace('✓', '<span style="color:#5affb8">✓</span>')
.replace(/(\?)/g, '<span style="color:#5affb8">$1</span>')
.replace(/(\[userName\])/g, '<span style="color:#5affb8">$1</span>');
const label = document.getElementById('code-panel-label');
label.textContent = mode === 'vuln' ? "The code — what's happening" : 'The code — fixed';
label.className = 'font-mono text-[11px] font-medium tracking-[0.12em] uppercase pb-3 border-b border-db-line ' + (mode === 'vuln' ? 'text-db-red' : 'text-db-green');
}
function onInput() {
updateSQLDisplay(document.getElementById('search-input').value);
}
function updateSQLDisplay(val) {
const box = document.getElementById('sql-display');
const attack = ATTACKS[val];
if (mode === 'safe') {
box.className = 'sql-box safe-q rounded px-3.5 py-3 font-mono text-[12.5px] leading-[1.7] whitespace-pre-wrap break-all min-h-[52px]';
box.innerHTML = `<span class="sql-kw">SELECT</span> * <span class="sql-kw">FROM</span> task <span class="sql-kw">WHERE</span> user_id = (<span class="sql-kw">SELECT</span> id <span class="sql-kw">FROM</span> user <span class="sql-kw">WHERE</span> name = <span class="sql-param">?</span>)\n<span style="color:#555;font-size:11px">-- bound value: </span><span class="sql-param">"${esc(val || '…')}"</span>`;
return;
}
const cls = 'sql-box rounded px-3.5 py-3 font-mono text-[12.5px] leading-[1.7] whitespace-pre-wrap break-all min-h-[52px]';
if (!val) {
box.className = cls;
box.innerHTML = `<span class="sql-kw">SELECT</span> * <span class="sql-kw">FROM</span> task <span class="sql-kw">WHERE</span> user_id = (<span class="sql-kw">SELECT</span> id <span class="sql-kw">FROM</span> user <span class="sql-kw">WHERE</span> name = '<span class="sql-str">…</span>')`;
return;
}
if (attack) {
box.className = cls + ' danger';
box.innerHTML = `<span class="sql-kw">SELECT</span> * <span class="sql-kw">FROM</span> task <span class="sql-kw">WHERE</span> user_id = (<span class="sql-kw">SELECT</span> id <span class="sql-kw">FROM</span> user <span class="sql-kw">WHERE</span> name = '<span class="sql-inject">${esc(val)}</span>')`;
} else {
box.className = cls;
box.innerHTML = `<span class="sql-kw">SELECT</span> * <span class="sql-kw">FROM</span> task <span class="sql-kw">WHERE</span> user_id = (<span class="sql-kw">SELECT</span> id <span class="sql-kw">FROM</span> user <span class="sql-kw">WHERE</span> name = '<span class="sql-str">${esc(val)}</span>')`;
}
}
function tryAttack(val) {
document.getElementById('search-input').value = val;
updateSQLDisplay(val);
runSearch();
}
function runSearch() {
const val = document.getElementById('search-input').value;
lastInput = val;
updateSQLDisplay(val);
if (!db) return;
const attack = ATTACKS[val];
const explainArea = document.getElementById('explain-area');
const analysisBox = document.getElementById('analysis-box');
const fixHint = document.getElementById('fix-hint');
if (mode === 'safe') {
try {
const stmt = db.prepare('SELECT t.id, t.title, u.name AS assigned_to, s.name AS status, t.due_date FROM task t LEFT JOIN user u ON u.id=t.user_id LEFT JOIN status s ON s.id=t.status_id WHERE u.name = ?');
stmt.bind([val]);
const rows = [];
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
renderResults(rows, false);
explainArea.innerHTML = `<div class="safe-box rounded px-3.5 py-3 text-[13px] leading-relaxed"><span class="mr-1.5">✓</span>Input treated as a plain string value. No matter what is typed, it is compared literally to the name column — it cannot modify the query structure.</div>`;
analysisBox.textContent = attack
? `The same attack string "${val}" is harmless here. It is passed as a bound value, so the database looks for a user literally named "${val}" — finds none — and returns zero rows. The SQL structure never changes.`
: 'The query runs safely. Input is bound separately and cannot alter the SQL.';
fixHint.style.display = 'none';
} catch(e) {
renderResults([], false);
explainArea.innerHTML = `<div class="neutral-box rounded px-3.5 py-3 text-[13px]">Error: ${e.message}</div>`;
}
return;
}
// Vulnerable mode
if (attack && (val.includes('DELETE') || val.includes('DROP'))) {
document.getElementById('result-badge').className = 'badge badge-danger px-2 py-0.5 rounded-sm text-[10px] font-medium';
document.getElementById('result-badge').textContent = 'destructive — simulated only';
document.getElementById('results-body').innerHTML = `<div class="text-center font-mono text-xs py-4" style="color:#ff6b6b">⚠ This query would ${val.includes('DELETE') ? 'delete all tasks' : 'drop the user table'} — not executed in this demo to preserve the data.</div>`;
explainArea.innerHTML = `<div class="danger-box rounded px-3.5 py-3 text-[13px] leading-relaxed"><span class="mr-1.5">⚠</span><strong>${attack.title}.</strong> ${attack.what}</div>`;
analysisBox.innerHTML = `<strong style="color:#ff6b6b">${attack.title}</strong><br><br>${attack.how}<br><br><span style="color:#555;font-size:12px">Severity: <span style="color:#ff6b6b">${attack.severity.toUpperCase()}</span></span>`;
fixHint.style.display = 'block';
return;
}
try {
let sql;
if (val.toUpperCase().includes('UNION')) {
sql = `SELECT t.id, t.title, u.name AS assigned_to, s.name AS status, t.due_date FROM task t LEFT JOIN user u ON u.id=t.user_id LEFT JOIN status s ON s.id=t.status_id WHERE u.name = '${val}'`;
} else {
sql = `SELECT t.id, t.title, u.name AS assigned_to, s.name AS status, t.due_date FROM task t LEFT JOIN user u ON u.id=t.user_id LEFT JOIN status s ON s.id=t.status_id WHERE t.user_id = (SELECT id FROM user WHERE name = '${val}')`;
}
const res = db.exec(sql);
const rows = res.length ? res[0].values.map(r => ({ id: r[0], title: r[1], assigned_to: r[2], status: r[3], due_date: r[4] })) : [];
renderResults(rows, !!attack);
if (attack) {
explainArea.innerHTML = `<div class="danger-box rounded px-3.5 py-3 text-[13px] leading-relaxed"><span class="mr-1.5">⚠</span><strong>${attack.title}.</strong> ${attack.what}</div>`;
analysisBox.innerHTML = `<strong style="color:#ff6b6b">${attack.title}</strong><br><br>${attack.how}<br><br><span style="color:#555;font-size:12px">Severity: <span style="color:#ff6b6b">${attack.severity.toUpperCase()}</span></span>`;
fixHint.style.display = 'block';
} else {
explainArea.innerHTML = `<div class="neutral-box rounded px-3.5 py-3 text-[13px]">Normal query — showing tasks for "${esc(val)}".</div>`;
analysisBox.textContent = 'Normal input. The query works as expected.';
fixHint.style.display = 'none';
}
} catch(e) {
document.getElementById('result-badge').className = 'badge badge-danger px-2 py-0.5 rounded-sm text-[10px] font-medium';
document.getElementById('result-badge').textContent = 'SQL error';
document.getElementById('results-body').innerHTML = `<div class="text-center font-mono text-xs py-4" style="color:#ff6b6b">Error: ${e.message}</div>`;
explainArea.innerHTML = `<div class="danger-box rounded px-3.5 py-3 text-[13px] leading-relaxed"><span class="mr-1.5">⚠</span>The injected SQL caused a syntax error — but not all injection attempts throw errors. Many succeed silently.</div>`;
fixHint.style.display = 'block';
}
}
function renderResults(rows, isAttack) {
const badge = document.getElementById('result-badge');
const body = document.getElementById('results-body');
badge.className = 'px-2 py-0.5 rounded-sm text-[10px] font-medium ' + (isAttack ? 'badge-danger' : rows.length ? 'badge-safe' : 'badge-empty');
if (!rows.length) {
badge.textContent = '0 rows';
body.innerHTML = '<div class="text-center font-mono text-xs text-db-muted py-4">No results</div>';
return;
}
badge.textContent = `${rows.length} row${rows.length !== 1 ? 's' : ''} ${isAttack ? '— leaked!' : 'returned'}`;
let h = '<table class="rtable"><thead><tr><th>id</th><th>title</th><th>assigned_to</th><th>status</th><th>due_date</th></tr></thead><tbody>';
rows.forEach(r => {
h += `<tr${isAttack ? ' class="highlight"' : ''}><td>${r.id ?? ''}</td><td>${esc(String(r.title ?? ''))}</td><td>${esc(String(r.assigned_to ?? ''))}</td><td>${esc(String(r.status ?? ''))}</td><td>${esc(String(r.due_date ?? ''))}</td></tr>`;
});
h += '</tbody></table>';
body.innerHTML = h;
}
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
init().catch(console.error);
</script>
</body>
</html>