Security
Lesson 1 of 8About 3 min readSuggest an edit

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 HttpOnly so 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=Lax or Strict on session cookies, which stops them from being sent on cross-site POSTs.
  • Checking the Origin header on state-changing requests.
  • Anti-CSRF tokens for older setups.
  • Never changing state on GET requests.

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.

Next: Authentication and sessions

Storing passwords safely, sessions versus tokens, cookie flags, multi-factor authentication and OAuth.