The request
A browser sends a request; your code reads the database and sends back a page. But anyone can send any request with any text in it, so treat every part of it as untrusted until your code has checked it.
Checkpoints on the way in
The first checkpoint validates input: is the id a whole number? Allow only what you expect and reject the rest. int("4x2") fails, so that request stops here with 400 Bad Request, an error sent back.
Broken access control tops the OWASP Top 10 (2021). Being logged in is not enough: on every request the server checks this user may see this record. User 7 asking for user 9's invoice gets 403 Forbidden.
Injection happens when code glues user text into a command. In "... WHERE id=" + n, the text becomes part of the SQL itself. Even an innocent name like O'Brien breaks such a query: its quote mark ends the text early.
Keep the command and the data apart. Python's database API sends the query with a ? placeholder and the value separately. The database fills the slot and always treats the value as data, never as part of the command.
Safe output
Cross-site scripting (XSS): if a page pastes user text straight into its HTML, the browser may run that text as code. Escape it first: html.escape turns < into < and > into >, so it shows as plain text.
Content-Security-Policy is a response header that tells the browser which scripts it may run. With script-src 'self', only scripts from your own site run, so if escaping is missed somewhere, injected script is still blocked.
Around the code
A password or API key typed into the source code is copied everywhere the code goes, its history included. Read it when the program starts, from an environment variable or a secret store, so the code can be shared and the secret is not.
Your app also runs code you did not write: libraries. Old versions can have known, published flaws. Keep a list of them, update them, and let a tool such as pip-audit check them against a database of known vulnerabilities.
Give each part only the access it needs. The web app's database account can read and write its tables but cannot delete them, so if something ever slips past every check, the damage stays small.
No single check catches everything, so secure code uses layers: validate input, check access on every request, keep SQL and data apart, escape output, add CSP, keep secrets and libraries in order, and grant least privilege.
What happens
;; step 1/11 · untrusted browser → your server → database a request carries text chosen by whoever sent it: the address, form fields, cookies, headers none of it can be trusted until it is checked checkpoints below: input · access · query · output
The card lines are short sketches of each pattern, not a whole program: escape is html.escape, environ is os.environ, deny() stands for answering 403 Forbidden, and q holds the query with a ? placeholder. Rankings and names come from the OWASP Top 10, 2021 edition. In that edition cross-site scripting is part of A03 Injection. OWASP revises the list every few years; the fixes shown here stay the same.