Expert · Performance, security and testing · topic 29 of 34 · 30 min
A full-stack security review
Explanation
Why a review, not one fix
Attackers do not need ten holes. They need one. A security review means you walk through every part of the app, frontend, backend, database and hosting, with a checklist, and fix what is missing. Do it before launch and after every big feature.
| Area | What to check in the Notes app |
|---|---|
| HTTPS everywhere | Every page and API call uses HTTPS. HTTP redirects to HTTPS. HSTS header is set. |
| Passwords | Stored only as password_hash made with bcrypt or Argon2. Never plain text, never in logs. |
| Sessions | Session id in a cookie with HttpOnly, Secure and SameSite=Lax. Logout deletes the session on the server. |
| Authorization | Every /api/notes/:id route checks the note belongs to the logged-in user. |
| Input validation | Server checks: title 1-100 characters after trimming, body up to 5000, pinned is a boolean. |
| Output escaping (XSS) | Note text is shown with textContent or React, never pasted into innerHTML. |
| CSRF | SameSite cookies plus a CSRF token, or a custom header check, on POST, PATCH and DELETE. |
| SQL | Only parameterised queries (placeholders like ?), never strings joined with user input. |
| Secrets | .env is in .gitignore. Keys live in the host's environment settings. |
| Dependencies | npm audit is clean of high issues. Dependabot (or Renovate) opens update pull requests. |
| Security headers | Content-Security-Policy with frame-ancestors, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy. |
| Rate limiting | Login, signup and OTP routes allow only a few tries per minute per IP and per account. |
| Logging | Logs have request ids and errors, but no passwords, OTPs, tokens or full note text. |
| Backups | Daily database backups, kept for some days, and a restore that you have actually tested. |
Authorization on every endpoint (no IDOR)
IDOR means Insecure Direct Object Reference. It happens when a user changes /api/notes/41 to /api/notes/42 in the URL and sees someone else's note. Being logged in is not enough: the server must check that this note belongs to this user, on every route.
// GET /api/notes/:id
app.get('/api/notes/:id', requireLogin, (req, res) => {
const note = db
.prepare('SELECT id, title, body, pinned FROM notes WHERE id = ? AND user_id = ?')
.get(Number(req.params.id), req.user.id);
// 404 (not 403) so we do not even reveal that note 42 exists
if (!note) return res.status(404).json({ error: 'Note not found' });
res.json(note);
});XSS: escape everything you show
// DANGEROUS: a note titled <img src=x onerror="stealCookies()"> would run code
card.innerHTML = '<h2>' + note.title + '</h2>';
// SAFE: the browser shows the text as text
const h2 = document.createElement('h2');
h2.textContent = note.title;
card.appendChild(h2);
// In React, {note.title} is escaped for you. Avoid dangerouslySetInnerHTML.Cookies and security headers
Set-Cookie: sid=8f2c...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=604800
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; img-src 'self' data:; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-originSecrets, updates, limits, logs and backups
- Secrets out of git: if a key was ever committed, deleting the file is not enough. Rotate (replace) the key, because the old one stays in git history.
- Dependency updates: run npm audit, turn on Dependabot alerts in GitHub, and update packages in small, regular steps.
- Rate limiting: for example, 5 login tries per minute per IP and per email. OTP routes need limits too, or someone can guess a 6-digit OTP or run up your SMS bill.
- Logging: log what happened (user id 17 failed login, request id abc), not secrets or personal data. Mask phone numbers and emails if you must log them.
- Backups: a backup you never restored is a hope, not a backup. Try a restore into a test database once a month.
Watch the lesson
A full-stack security review · 1/6
Attackers need only one open window. Today we walk around the whole Notes app with a security checklist.
Prefer a video by a teacher? Find free videos on “web application security checklist OWASP top 10 for developers” on YouTube ↗
In real life: The note that showed someone else's data
A student team launched a shared expense app for hostel rooms. Each bill had a URL like /bills/1052. One user noticed that changing it to /bills/1051 showed another room's bills, with names and UPI ids. The API checked that the user was logged in, but not that the bill belonged to their room. The fix was one extra condition in every query (AND room_id = ?) and a test for each route that logs in as user A and asks for user B's data. That one test would have caught the bug before launch.
Practice (0/3 done)
Write checkSecurityHeaders(headers). headers is an object of response headers (names may be in any letter case). Return an array of what is missing, in this order: "content-security-policy", "strict-transport-security", "x-content-type-options", "referrer-policy". Then, if the Content-Security-Policy value does not contain "frame-ancestors" (or there is no CSP), also add "frame-ancestors". Return [] when nothing is missing.
Write reviewCode(source), a tiny code reviewer. Check each line (numbered from 1) and return a list of { line, rule } objects, in line order. Rules, checked in this order on each line: "innerHTML" when the line uses .innerHTML, except a plain clear like list.innerHTML = ''; "sql-concat" when the line has the word SELECT, INSERT, UPDATE or DELETE (any case) and also a + or ${ (text joined into SQL); "hardcoded-secret" when the line sets something named like api_key, apiKey, secret, password or token to a quoted value of 8 or more characters.
Do a security review of your own Notes app (or another app you built) using the checklist in this lesson. Write down each item as pass, fail or not sure, and fix at least two fails.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.