Skip to content

Expert · Performance, security and testing · topic 29 of 34 · 30 min

A full-stack security review

Explanation

A security review is going through your whole app with a checklist before launch, and again every few months. For the Notes app that means HTTPS, hashed passwords, safe cookies, checking ownership on every endpoint, validating input, escaping output, parameterised SQL, keeping secrets out of git, updating packages, setting security headers, rate limiting logins, careful logs and backups.

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.

Think of it like this: A strong lock on your front door does not help if the kitchen window is open. A thief walks around the house and tries every window. A security review is you walking around your own house first.
AreaWhat to check in the Notes app
HTTPS everywhereEvery page and API call uses HTTPS. HTTP redirects to HTTPS. HSTS header is set.
PasswordsStored only as password_hash made with bcrypt or Argon2. Never plain text, never in logs.
SessionsSession id in a cookie with HttpOnly, Secure and SameSite=Lax. Logout deletes the session on the server.
AuthorizationEvery /api/notes/:id route checks the note belongs to the logged-in user.
Input validationServer 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.
CSRFSameSite cookies plus a CSRF token, or a custom header check, on POST, PATCH and DELETE.
SQLOnly parameterised queries (placeholders like ?), never strings joined with user input.
Secrets.env is in .gitignore. Keys live in the host's environment settings.
Dependenciesnpm audit is clean of high issues. Dependabot (or Renovate) opens update pull requests.
Security headersContent-Security-Policy with frame-ancestors, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy.
Rate limitingLogin, signup and OTP routes allow only a few tries per minute per IP and per account.
LoggingLogs have request ids and errors, but no passwords, OTPs, tokens or full note text.
BackupsDaily 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.

javascript
// 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);
});
Runs on a server with Node.js: ownership is part of the query, and the SQL uses placeholders.

XSS: escape everything you show

javascript
// 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.
Runs in the browser: XSS (cross-site scripting) happens when user text is treated as HTML.

Cookies and security headers

output
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-origin
Response headers of a well set up Notes app. HttpOnly hides the cookie from JavaScript; frame-ancestors stops other sites putting your app in a frame.

Secrets, 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.
Tip: You do not have to remember everything. The OWASP Top 10 lists the most common web risks, and the OWASP Cheat Sheet Series gives short, practical fixes for each one (passwords, sessions, XSS, CSRF and more).
Careful: Client-side checks are for user comfort only. Anyone can skip your React form and call the API with curl. Every rule (who owns a note, title length, rate limits) must also be enforced on the server.

Watch the lesson

A full-stack security review · 1/6

One hole is enough Check everything

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)

Task 1 · Code · Javascript

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.

Task 2 · Code · Javascript

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.

Task 3 · Practice

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

Question 1

1.User A changes /api/notes/41 to /api/notes/42 and sees user B's note. What is this bug called?

Question 2

2.Which cookie setting stops JavaScript on the page from reading the session cookie?

Question 3

3.A secret API key was committed to git last month. What is the right fix?

Question 4

4.Which header tells browsers to always use HTTPS for your site?

Question 5

5.What is a good thing to write in your server logs after a failed login?

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.

← Previous: Making the Notes app fast