SecureFix · a teach-through walkthrough

Six ways to break a web app, and how to fix every one.

A tiny invoice API with six real security bugs planted in it. For each bug you'll see how an attacker breaks in, how the fix shuts them out, and how a robot reviewer catches it before it ever ships again. No security background needed.

TypeScript + Express 6 vulnerability classes exploit → fix → detect → prevent
00

Start here

What is this, in one breath

Imagine a small web app that stores invoices. It works fine. But hidden inside it are six classic security bugs — the same kinds that show up in real companies every week. SecureFix is a guided tour of those six bugs.

The trick is that it shows both sides of the desk. First the attacker's view: the exact trick that breaks the app. Then the engineer's view: the fix, and an automatic guard that fails the build if anyone reintroduces the bug. Learning one side without the other is how bugs keep coming back. This does both.

First idea to hold onto

A web app is a waiter. You (the browser) send a request — "give me invoice 3", "search for 'domain'" — and the server goes to the kitchen (the database), does what you asked, and brings back the result. Every bug here is a way to make the waiter do something it shouldn't, by wording the request cleverly.

01

The method

The loop, repeated six times

Every bug in this project goes through the same four steps. Once you see the loop once, the other five are variations on it.

01 · attack
Exploit
Write a test that actually breaks in and proves the bug is real.
02 · defend
Fix
Change the code so the same trick stops working.
03 · detect
Rule
Teach a code-scanner the shape of this bug so it spots the next one.
04 · prevent
Gate
Wire the scanner into CI so a bad change fails the build automatically.

find it → fix it → teach a robot to catch it → block it forever

02

The playground

The app you'll be breaking

It's deliberately boring: an invoice service with two users and three invoices. Boring is the point — it keeps your attention on the bug, not on a maze of features.

Users

aliceid 1
bobid 2

Invoices

#1 · Cloud hosting owner: alice
#2 · Domain renewal owner: alice
#3 · Pentest engagement owner: bob

Keep this cast in mind: whenever alice manages to read invoice #3, she's read something that belongs to bob. That's the whole shape of several bugs below.

03

The six bugs · click through

Break it, then fix it

Each tab is one bug. You'll get a plain-English analogy, the trick that breaks it, the fix side by side with the broken code, and the rule that catches it next time.

SQL injection — the search box that runs commands

CWE-89 · OWASP A03 Injection · severity High
Analogy

You type a word into a search box. But the app pastes your word straight into an instruction it hands the database. So instead of a word, you type your own instruction — and the database obeys it. Like writing "one coffee, AND give me everything in the till" on the order slip, and the barista just does it.

THE ATTACK

1
Search is built by gluing your text into the query: ...LIKE '%YOUR TEXT%'
2
So you send text that closes the quote and adds a command: x%' UNION SELECT password, username, 0 FROM users --
3
The database returns every user's password hash, dressed up as "invoices".
Broken
db.prepare(
  `SELECT id, description, amount
   FROM invoices
   WHERE description LIKE '%${q}%'`
).all();  // q is glued in raw
Fixed
db.prepare(
  'SELECT id, description, amount
   FROM invoices
   WHERE description LIKE ?'
).all(`%${q}%`); // q is bound, not parsed

The fix uses a ? placeholder. The database treats q as a value to search for, never as part of the command. A quote or a UNION in your input is now just text.

How the robot catches it
sqli-template-literal-in-query

Flags any query string built with a ${...} placeholder inside .prepare() — the exact "glued in raw" shape. The safe version has no ${...} in the SQL, so it stays silent.

04

The robot reviewer

How the code-scanner "thinks"

The tool doing the catching is Semgrep. Think of it as a spell-checker, but for dangerous code shapes instead of typos. You write a rule once, and it reads every file looking for that shape. Two flavours show up in this project.

Pattern rules — match a shape

The simplest rules describe a shape of code, like "a query built with ${...} glued inside it." Fast and blunt. Good for the obvious cases, but they can't tell whether the data is actually dangerous — only what the code looks like.

Taint rules — follow the dye

The smarter rules (SSRF, XSS) trace a drop of dye. They mark anything from the request as "tainted", then watch to see if it reaches a dangerous place — a fetch(), an HTML response — without passing through a cleaning step first. If it does, that's the bug. This is why the fix works by routing the value through a named cleaner: the tracker sees it get washed and stops worrying.

The honest part

Writing the rules was where the real work hid. Three of these rules were shipped but had never actually been run — and two of them caught nothing until they were tested against a live bug. A scanner you never verify is a false sense of safety. Every rule here was checked to fire on the bug and stay silent on the fix.

05

The safety net

The gate: red before, green after

The project keeps two copies of the code. One has all six bugs; one has all six fixes. The exact same automated pipeline runs on both — and that's the proof the loop works.

seed · all bugs FAILS
typecheck clean
tests exploits fire
scanner 7 findings
main · all fixed PASSES
typecheck clean
tests exploits blocked
scanner 0 findings

On the buggy copy the scanner finds seven problems and fails the build (red). On the fixed copy it finds nothing and the build passes (green). Wire that scanner into CI and nobody can merge one of these bugs back in without the build going red first. That's the "prevent" step — the bug is caught by a machine, not by hoping a human notices.

Why seven findings for six bugs? Prototype pollution trips two separate rules.

That's the whole thing

Six bugs, each taken all the way around the loop: a break-in you can run, a fix that stops it, a rule that catches the next one, and a gate that blocks it forever. If you followed the SQL injection tab from top to bottom, you've already seen the pattern the other five repeat.

To poke at it yourself, the two copies live on git branches seed/all-vulns and main:

git checkout seed/all-vulns && npm run test:security && npm run sast:docker:rules