0 / 91
Week 11 · Day 75 of 91

Security review with OWASP

Testing, Security, Docker, and Deployment

Objective

Use a current risk checklist to inspect the application realistically.

Production readiness resembles validation before field deployment: repeatable setup, protective limits, test evidence, monitoring, and rollback plans.

  • access control
  • injection
  • security misconfiguration
  • authentication failures

Why this matters

Your app now holds other people's accounts behind a login. That changes what a bug means: a broken button annoys someone, a broken permission check exposes their data. Today you inspect your app against a current, evidence-based risk list — the OWASP Top 10 — fix one real issue, and write down honestly what is still weak. By the end you will have a checklist a stranger could act on.

What the OWASP Top 10 is, and how to read it

The OWASP Top 10 is a periodically republished list of the most significant categories of web application security risk, compiled from data across many real applications. It is not a list of specific bugs and not a certification. It is a set of classes of flaw, ordered by how often they appear and how badly they hurt.

Two things follow from that, and both matter today:

  1. The numbering changes between editions. A category that was A03 in one edition may be A05 in the next. Cite the edition and check the current position in the reference rather than trusting a number you remember; you will do exactly that at the end of this lesson.
  2. Categories are prompts, not tests. "Broken Access Control" does not tell you which of your routes is broken. It tells you where to go looking.

Today covers the four categories most likely to bite an app shaped like yours: broken access control, injection, security misconfiguration, and authentication failures.

Failure modes, not a pass/fail sticker

A component datasheet lists failure modes — overvoltage, reverse polarity, thermal runaway — so you design a protection for each. It does not certify your board. The Top 10 is that failure-mode list for web applications: it tells you which protections a reviewer expects to find, and leaves the checking to you.

Broken access control

Authentication is who are you. Authorization, also called access control, is what are you allowed to do. Broken access control has been the top category for years running, because it is the one flaw a normal, logged-in user finds by accident.

Two shapes:

  • Horizontal — user A reads or edits user B's data, because the route looks up a record by id and never checks who owns it. This is an insecure direct object reference: the id in the URL is the only thing standing between two users' data.
  • Vertical — an ordinary user performs an administrator action, because the check lived in the UI ("we hide the Delete button") rather than the server.

The Day 1 fact still rules here: the user controls the browser. A hidden button is a suggestion. The fix belongs in the query itself, so it cannot be forgotten by a later route:

// vulnerable: any id belonging to anyone
await pool.query("SELECT * FROM equipment WHERE id = $1", [id]);

// enforced: ownership is part of the lookup
await pool.query("SELECT * FROM equipment WHERE id = $1 AND owner_id = $2", [id, req.user.id]);

Injection

Injection happens when data supplied by a user is treated as code by something downstream. It is not one bug; it is a family, and SQL and HTML are the two you own.

SQL injection comes from building a query by gluing strings together:

// vulnerable
await pool.query(`SELECT * FROM equipment WHERE serial = '${serial}'`);

A serial of ' OR 1=1 -- turns that into a query returning every row. The fix is the one you have been using since Day 46: parameterized queries, where $1 is a placeholder and the driver sends the value separately, so it can never be parsed as SQL.

await pool.query("SELECT * FROM equipment WHERE serial = $1", [serial]);

Cross-site scripting (XSS) is injection into the page: user-supplied text ends up interpreted as HTML or JavaScript. React escapes text by default, which is why you have not met this yet — but dangerouslySetInnerHTML opts out, and the name is a warning, not a decoration.

Dictating a letter

You dictate "Dear Sam, new paragraph" and the typist starts a new paragraph — dictation mixes the message with the instructions, so a name can become a command. Parameterized queries hand the typist a sealed envelope marked "this is text, never an instruction".

Security misconfiguration

The code is correct and the deployment is wrong. This category is boring and extremely common:

  • Stack traces returned to clients, naming your file paths and libraries.
  • Permissive CORS (origin: "*" alongside credentials), so any site can call your API as your user.
  • Session cookies missing httpOnly, Secure, or SameSite, which you met on Day 53.
  • Default or example credentials left enabled.
  • Secrets in the repository, or logged. Git history keeps them after you delete the line.
  • Dependencies never updated. npm audit reports known vulnerabilities in what you installed.

Authentication failures

Login is a target because it is the front door. What reviewers look for:

  • No rate limiting. Unlimited password guesses at machine speed. A per-IP and per-account limit on the login route is the standard mitigation.
  • Passwords stored recoverably. They must be hashed with a slow, salted algorithm such as bcrypt or argon2 — the Day 51 work. If a password can be emailed back, it is stored wrongly.
  • Sessions that never end. A token with no expiry is a permanent key.
  • Errors that leak account existence. "No such user" versus "wrong password" tells an attacker which emails are registered. Return one message for both.

What a finding must contain

A security finding that says "authentication is insecure" is unusable. A finding is only actionable when it states four things — affected behaviour, evidence, impact, and mitigation — plus a severity and, honestly, what remains unfixed.

Field What goes in it
Affected behaviour The exact route or screen, and what it does
Evidence The command you ran and the response you got
Impact What an attacker gains, in plain words
Severity High / Medium / Low, justified by impact and how easy it is
Mitigation The specific change, and the test that now covers it
Remaining limitation What you did not fix, and why

Only probe systems you are authorised to test

Everything today runs against your own app on your own machine. Running these probes against a service you do not own is unauthorised access and is illegal in most jurisdictions, regardless of intent. If you want a practice target beyond your own app, use a project explicitly published for the purpose, such as OWASP Juice Shop, on your own machine.

Walkthrough: find one real access-control flaw

Start your app locally and register two users. Log in as each and keep both tokens.

TOKEN_A=$(curl -s -X POST http://localhost:3000/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"correct-horse-battery"}' | jq -r .token)

Repeat for TOKEN_B. As user A, create equipment and note its id. Now do what a curious user would — ask for A's record while holding B's token:

curl -i -H "Authorization: Bearer $TOKEN_B" http://localhost:3000/api/equipment/1

If your app is vulnerable you will see:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":1,"name":"Pump 3","serial":"SN-1001","ownerId":2}

That 200 is your evidence, and ownerId proving the row belongs to someone else is the impact, in one response. Fix it by adding AND owner_id = $2 to the lookup, restart, and re-run the exact same command:

HTTP/1.1 404 Not Found

404 rather than 403 is a deliberate choice: 403 confirms the record exists, which leaks information. Either is defensible; write down which you chose and why. Then lock it in with a test — you wrote the forbidden case on Day 73, so add this to that file and watch it go green.

Checkpoint

You have a before response, an after response, and a test that fails if the fix is reverted. Those three artifacts are what turns an opinion into a finding.

Your turn

Build docs/security-review.md using the six-column table above.

  1. Access control. Probe two routes with the wrong user's token, and one admin-only action with a normal account. Record status codes as evidence — for the fixed one and any still broken.
  2. Injection. Search your server code for template literals inside pool.query. Record every hit. Then check your React code for dangerouslySetInnerHTML. Note that a clean search is itself a finding, recorded as "checked, none found".
  3. Misconfiguration. Trigger a 500 deliberately and record whether the response body contains a stack trace. Check your CORS origin, your cookie flags, and run npm audit; record the count of high-severity advisories.
  4. Authentication failures. Send ten wrong-password logins in a row and record whether the eleventh is still accepted. Check whether a wrong email and a wrong password give different messages.
  5. Fix exactly one issue, end to end: change the code, capture the new evidence, and add a test that fails when you revert the fix.
  6. Fill in the Remaining limitation column for everything you did not fix, with a one-line reason. An honest list of known gaps is more useful than a clean-looking one.

Reviewer mode — after your own pass, never before

"Perform a focused security review. Do not claim vulnerabilities without a plausible path and evidence."

A review must produce specific, actionable findings with evidence — never general praise and never a wholesale rewrite. Do your own pass first, or you will only check a generated list. Expect confident, plausible findings that are wrong for your app; for each one, demand the attacker's path, then reproduce it with curl yourself. A finding you cannot reproduce does not go in the table.

You are done when

Every row has evidence you personally captured, one issue is fixed with a test protecting it, and the gaps you left are written down.

Common pitfalls

  • Checking permission in the UI only. Hiding a button changes nothing; the request still works. Enforce on the server, ideally in the query.
  • Treating the Top 10 as a checklist to tick. It is a prompt list. The finding is what you observed in your app, with evidence.
  • Reporting severity by vibe. Severity comes from impact plus ease. "Any logged-in user can read every record with one URL change" is high; "an admin can see a stack trace" is not.
  • Committing a secret while fixing a config issue. Rotating the key is the only real fix once it is in Git history — deleting the line does not remove it from earlier commits.

Verify it yourself

Open today's reference, the OWASP Top 10 (2025 edition).

  1. Find the current position and exact title of the four categories this lesson covered. Record the edition and the identifiers (A01, A02, …) next to each row of your table — this lesson deliberately did not give you the numbers.
  2. Pick one category from the list that today's lesson did not cover, read its description, and write two sentences on whether it applies to your app and how you would check.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Review the app against relevant OWASP Top 10 categories and fix one concrete issue.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

A security checklist with evidence, severity, fix, and remaining limitations.

Working with AI today

AI as skeptical reviewer

Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.

Perform a focused security review. Do not claim vulnerabilities without a plausible path and evidence.

References

End-of-day quiz

Q1 What should security findings include?
Q2 Which result best proves today’s work is complete?
Q3 What should an AI code review primarily produce?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.