Common web security risks
The OWASP Top 10 lists the most common serious risks in web applications. A handful of them cause most real-world breaches, and each has a well-known defence.
Injection
Injection happens when user input is treated as code. SQL injection is the classic case.
// Vulnerable: the input becomes part of the SQL
const rows = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// email = "' OR '1'='1" returns every user
// Fixed: a parameterised query sends the value separately from the SQL
const rows = await db.query("SELECT * FROM users WHERE email = ?", [email]);
Parameterised queries (prepared statements) are the fix. Escaping by hand is error-prone. The same principle applies to shell commands, LDAP queries and template engines: never build code by concatenating strings from users.
Broken access control
This is the most common risk in the current list. The app authenticates the user but forgets to check whether this user may touch this resource.
// Vulnerable: anyone signed in can read any invoice by changing the id
app.get("/invoices/:id", requireLogin, async (req, res) => {
res.json(await invoices.find(req.params.id));
});
// Fixed: scope the lookup to the current user
app.get("/invoices/:id", requireLogin, async (req, res) => {
const invoice = await invoices.findForOwner(req.params.id, req.user.id);
if (!invoice) return res.sendStatus(404);
res.json(invoice);
});
Check authorisation on the server for every request. Hiding a button in the UI is not access control. Returning 404 instead of 403 for other people’s resources also avoids confirming that they exist.
Cross-site scripting (XSS)
XSS happens when attacker-controlled text is inserted into a page as HTML, so it runs as script in other users’ browsers and can steal their sessions or act as them.
- Let your framework escape output. React, Vue, Svelte and Astro escape interpolated text by default.
- Treat raw HTML APIs (
innerHTML,dangerouslySetInnerHTML,v-html) as dangerous. If you must render user HTML, sanitize it with a vetted library. - Add a Content Security Policy as a second layer, and mark session cookies
HttpOnlyso scripts cannot read them.
Cross-site request forgery (CSRF)
A malicious site makes the victim’s browser send a request to your site, and the browser attaches your cookies automatically. Defences:
SameSite=LaxorStricton session cookies, which stops them from being sent on cross-site POSTs.- Checking the
Originheader on state-changing requests. - Anti-CSRF tokens for older setups.
- Never changing state on
GETrequests.
Secrets
- Never commit secrets. Anything pushed to a repository should be treated as leaked, even if you delete it later, because it stays in history.
- Load secrets from the environment or a secret manager, and give each environment its own.
- Rotate a secret as soon as you suspect it leaked.
- Store passwords only as slow, salted hashes (bcrypt, scrypt, Argon2), never encrypted or in plain text. Store API tokens and session identifiers as hashes too, so a leaked database cannot be used directly.
The mindset
Treat every input as hostile until validated, at the server, on every request. Give every component the least privilege it needs. Layer defences so one mistake is not fatal.