Without notes, state yesterday’s main idea and one unresolved question.
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:
- 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.
- 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, orSameSite, 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 auditreports 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.
- 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.
- Injection. Search your server code for template literals inside
pool.query. Record every hit. Then check your React code fordangerouslySetInnerHTML. Note that a clean search is itself a finding, recorded as "checked, none found". - 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. - 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.
- 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.
- 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).
- 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. - 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
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Review the app against relevant OWASP Top 10 categories and fix one concrete issue.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
A security checklist with evidence, severity, fix, and remaining limitations.
Working with AI today
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
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.