Before you start: Basic web requests and introductory programming. Use only your own app or an explicitly authorized lab.
Your toolkit: Threat modeling · access control · secure coding
Your learning path
1Start with a threat model
List what you protect, who can access it, and where untrusted input enters. For a notes app, private notes and session cookies are assets. Map browser → API → database and identify each trust boundary. Security work becomes clearer when tied to a concrete asset.
Try it: Draw a notes-app data flow. Choose three likely failures and one control for each.
2Authentication is not authorization
Authentication establishes identity. Authorization decides which actions that identity may perform on a resource. Enforce access checks on every server request. A hidden button or unpredictable record ID is not an access-control mechanism.
Try it: Use two local test accounts. Verify account A cannot read account B’s note by changing the URL ID.
3Keep data out of executable contexts
Parameterized queries separate SQL from values. Safe text rendering separates user text from HTML. Validation is useful, but escaping must match the output context. Avoid treating a string from a user as SQL, HTML, or a shell command.
Try it: Compare string-concatenated SQL with the parameterized SQLite example below. Use harmless input in your own local app.
4Protect sessions and credentials
Use established authentication libraries. Store password hashes with an appropriate password-hashing algorithm rather than plain text or a fast general-purpose hash. Use Secure, HttpOnly, and appropriate SameSite cookie settings; cookie authentication also needs a CSRF strategy.
Try it: Document session expiry and logout behavior. Confirm logout invalidates the session on the server.
5Handle secrets and dependencies
Keep secrets out of frontend bundles and Git history. If a secret leaks, revoke or rotate it; deleting the line is not enough. Review dependency updates and lockfiles, and remove unused packages. Log errors without passwords, session tokens, or private documents.
Try it: Check your own repository for accidentally committed credentials without pasting any findings into public logs.
6Turn controls into regression checks
A fix should survive future changes. Add tests for ownership checks, rejected invalid input, and safe rendering. Security headers are defense in depth; they cannot replace correct authorization or query handling. Practice only against applications you own or have permission to assess.
Try it: Write a regression checklist for the notes app and rerun it after changing a route.
A worked example
Use this small example to connect the lessons to code. Read the comments before running it.
import sqlite3
with sqlite3.connect(":memory:") as db:
db.execute("CREATE TABLE notes (owner_id INTEGER, title TEXT)")
db.execute("INSERT INTO notes VALUES (?, ?)", (7, "Hello"))
rows = db.execute(
"SELECT title FROM notes WHERE owner_id = ?", (7,)
).fetchall()
print(rows) # [('Hello',)]
PUT IT INTO PRACTICE
A security review of your own notes app
Build the project described in the lessons, then verify these acceptance criteria:
Ownership checks run on every private route.
Queries bind values as parameters.
Secrets and session tokens never appear in logs.
Deliverable: your source code, a short README, and evidence for each check. Explain one tradeoff you made.