100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Web Application Security
65 minintermediate

Practice — Finding and Patching SQLi and XSS in a Vulnerable App

What You'll Build

In this lab you will take a small, deliberately vulnerable Node.js application containing one SQL injection flaw and one stored XSS flaw, confirm both as an attacker would, and then patch them the right way. You will finish with a hardened version and automated tests that prove each vulnerability is closed and stays closed.

Analogy🏏Cricket
💪 Think of it like fitness: Assessment day is when the training gets tested for real, you take the map of your programme and put each targeted lift under load to see which strengths hold and which weaknesses are genuine, recording every number. Just as the measured test turns a plan into proven results, using your attack-surface map to probe injection, authentication, and API authorization, and confirming each real flaw with captured evidence, turns recon into demonstrated findings. This reveals where knowledge becomes proof: you deliberately load each mapped weakness and record exactly what gives way.

The point is to complete the full loop from Module 2: find, exploit safely, fix at the root, and verify. Rather than memorising payloads, you will practise the reasoning, spotting where data crosses into a code context, and applying parameterisation and output encoding as the structural cures. That reasoning transfers to any language or framework you meet later.

Analogy🏏Cricket
🏏 Think of it like cricket: A disciplined bowling attack does not spray deliveries everywhere and hope; guided by the scouting report, it targets each batter's flagged weakness, confirms the ball is doing what was planned, and records every dismissal for the analysts. Just as that planned, evidence-backed attack turns a dossier into wickets rather than wasted overs, working your test plan area by area, confirming each real flaw by safely triggering it, and documenting it rigorously turns recon into evidenced findings. This reveals the method: targeted, confirmed, and recorded beats spray-and-pray every time.

Prerequisites

  • Node.js 18+ and npm installed, with a terminal you are comfortable running commands in.
  • Completion of the Module 2 readings on SQL injection, parameterised queries, and XSS, whose concepts you will apply directly.
  • Basic familiarity with reading JavaScript and running a local web server on a port such as 3000.
  • An understanding that you must only run these techniques against this local lab or apps you are explicitly authorised to test.

Setup & Project Structure

Create a minimal Express application with a SQLite database and two endpoints: a login that is vulnerable to SQL injection, and a comments feature that is vulnerable to stored XSS. Keeping the app tiny makes the flaws easy to see and the fixes easy to verify. Run it locally and keep your browser's developer tools open so you can watch requests and responses throughout.

Analogy🏏Cricket
📷 Think of it like photography: A professional does not trust memory on a shoot; every frame is captured with its settings, the aperture, the shutter, the exact scene, so the shot can be reproduced and proven later. Just as that per-frame metadata makes each image a self-contained, repeatable record, working your test plan area by area and capturing the exact request, the response that proves impact, the reproduction steps, and the affected endpoint makes each finding a self-contained record. This reveals the discipline: evidence recorded at the moment of capture is what makes a finding stand up in the report.
bash
# Scaffold the lab locally
mkdir sqli-xss-lab && cd sqli-xss-lab
npm init -y
npm install express better-sqlite3
# Create app.js with two intentionally vulnerable endpoints
# (login via string-concatenated SQL, comments via unescaped HTML),
# then run it and open the developer tools Network tab:
node app.js          # serves on http://localhost:3000

Step 1 — Foundation: The Vulnerable App

Start from the vulnerable baseline so you can see each flaw before fixing it. The login handler concatenates the username and password straight into a SQL string, and the comments handler stores and renders comment text as raw HTML. Read both handlers carefully and predict, from Module 2, exactly how each could be exploited before you try. Naming the flaw first sharpens the fix.

Analogy🏏Cricket
♟️ Think of it like chess: A student studies a lost position and is asked to name the winning combination before touching a piece, because predicting the tactic sharpens understanding far more than stumbling onto it by trial. Just as calling the move in advance turns a vague sense of danger into concrete insight, reading the vulnerable login and comments handlers and predicting exactly how each is exploited before running anything turns a hunch into a precise plan. This reveals why the exercise asks you to name the flaw first: a vulnerability you can articulate is one you already half understand how to close.
javascript
// app.js — VULNERABLE baseline (study before exploiting)
// SQLi: username/password concatenated into the query
app.post('/login', (req, res) => {
  const { user, pass } = req.body;
  const row = db.prepare(
    `SELECT * FROM users WHERE user = '${user}' AND pass = '${pass}'`
  ).get();                                   // input becomes SQL
  res.send(row ? 'Welcome ' + row.user : 'Denied');
});

// Stored XSS: comment text rendered as raw HTML
app.post('/comment', (req, res) => {
  db.prepare('INSERT INTO comments(text) VALUES (?)').run(req.body.text);
  const all = db.prepare('SELECT text FROM comments').all();
  res.send(all.map(c => `<p>${c.text}</p>`).join(''));  // unescaped
});

Step 2 — Core Logic: Confirm the Flaws

Now confirm each flaw safely against your local app. For the login, submit a classic tautology in the password field so the WHERE clause becomes always true and you are logged in without valid credentials. For the comments, submit text containing a script or an image error handler and reload the page to see it execute, confirming stored XSS. Record exactly what you sent and what happened.

Analogy🏏Cricket
🍳 Think of it like cooking: A chef never assumes a dish is underseasoned; they taste a controlled spoonful first, confirming the exact problem before adjusting, so the correction targets what is actually wrong. Just as the deliberate taste turns a suspicion into a confirmed fact, submitting a tautology in the password field to force the WHERE clause always true, and posting a script payload then reloading to watch it run, confirms the SQL injection and stored XSS as demonstrated facts. This reveals why safe confirmation comes before patching: you record exactly what you sent and saw, so the fix answers a proven flaw, not a guess.
bash
# Confirm SQLi: a tautology makes the WHERE clause always true
curl -s http://localhost:3000/login \
  -d "user=admin" --data-urlencode "pass=' OR '1'='1"
# Expected: "Welcome admin" — logged in with no valid password.

# Confirm stored XSS: submit a payload, then reload to see it run
curl -s http://localhost:3000/comment \
  --data-urlencode "text=<img src=x onerror=alert(1)>"
# Reload the page in the browser: the onerror handler fires.

Step 3 — Integration & Enhancement: Patch at the Root

Fix each flaw at its structural root, not with a filter. Replace the concatenated login query with a parameterised statement so the credentials are bound as data and a tautology becomes a harmless literal that matches no user. Replace the raw HTML rendering with contextual output encoding, so comment text is displayed as text and any tags are shown rather than executed. Re-run your Step 2 payloads to see them fail.

Analogy🏏Cricket
💰 Think of it like finance: An auditor who keeps catching individual bad transactions eventually fixes the account's control itself, so the entire category of error becomes impossible rather than caught case by case. Just as repairing the underlying control closes the whole class of loss, replacing the concatenated login with a parameterised statement that binds credentials as data, and swapping raw HTML rendering for contextual output encoding, closes the whole class of injection so your Step 2 payloads simply fail. This reveals why root fixes beat filters: they neutralise every variant of the attack at once, not just the one payload you happened to try.
javascript
// PATCHED login — parameterised, credentials bound as data
app.post('/login', (req, res) => {
  const { user, pass } = req.body;
  const row = db.prepare(
    'SELECT * FROM users WHERE user = ? AND pass = ?'
  ).get(user, pass);                         // values never parsed as SQL
  res.send(row ? 'Welcome ' + row.user : 'Denied');
});

// PATCHED comments — contextual HTML encoding on output
function esc(s) {
  return s.replace(/[&<>"']/g, ch => ({
    '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
  }[ch]));
}
app.post('/comment', (req, res) => {
  db.prepare('INSERT INTO comments(text) VALUES (?)').run(req.body.text);
  const all = db.prepare('SELECT text FROM comments').all();
  res.send(all.map(c => `<p>${esc(c.text)}</p>`).join(''));  // rendered as text
});

Step 4 — Testing & Verification: Prove It Stays Fixed

Finally, lock the fixes in with automated tests, so a future change cannot silently reopen either hole. Write one test asserting the tautology login is now denied, and another asserting a submitted script payload comes back HTML-encoded rather than as live markup. A green test suite is your evidence that both vulnerabilities are closed, and your early warning if anyone ever regresses them.

Analogy🏏Cricket
⚽ Think of it like sports: A team that has drilled a defensive set does not trust it to memory; they run it repeatedly in training so any lapse shows up before match day, giving early warning of a weakness. Just as repeated drills prove the defence still holds and flag it the moment it slips, automated tests asserting the tautology login is denied and the script payload comes back HTML-encoded prove both flaws stay closed and warn you the instant a future change reopens them. This reveals why the lab ends in a test suite: a green run is standing evidence the vulnerabilities are shut, not a one-time hope.
javascript
// verify.test.js — proves both flaws are closed and stay closed
test('SQLi tautology is rejected after parameterisation', async () => {
  const res = await post('/login', { user: 'admin', pass: "' OR '1'='1" });
  expect(res.text).toBe('Denied');           // no auth bypass
});

test('stored XSS payload is HTML-encoded on output', async () => {
  await post('/comment', { text: '<img src=x onerror=alert(1)>' });
  const page = await get('/comment');
  expect(page.text).toContain('&lt;img');    // shown as text, not run
  expect(page.text).not.toContain('<img src=x onerror'); // not live markup
});

Warning: Run this lab only against your own local copy. The payloads here are illustrative and safe on your machine, but sending them to any application you do not own or have explicit written permission to test is illegal in most jurisdictions. Keep the server on localhost and never point these requests at a third party's site.

Extension Challenge: Extend the lab three ways. Add a Content Security Policy header that forbids inline scripts and confirm it would block the XSS payload even without the encoding fix, demonstrating defence in depth from Lesson 10. Replace your hand-written escape function with a framework's auto-escaping template and re-run the tests. Finally, add a search endpoint, introduce a blind SQLi into it, and prove parameterisation closes the timing channel from Lesson 7.

  • The full security cycle is find, safely confirm, fix at the root, and verify — a fix that is not tested is incomplete.
  • A SQL injection login is confirmed with a tautology that makes the WHERE clause always true, bypassing authentication.
  • Stored XSS is confirmed by submitting a payload such as an image error handler and reloading to see it execute for every viewer.
  • Parameterisation fixes SQLi structurally by binding credentials as data, so a tautology becomes a harmless literal that matches no user.
  • Contextual output encoding fixes XSS by rendering user text as text, so submitted tags are displayed rather than executed by the browser.
  • Automated tests lock the fixes in, proving both holes are closed and providing an early warning if a future change reopens them.
Lesson 12 of 35
0% complete