Web Security
Web Security

OWASP Top 10:2025 in a TypeScript stack

Card · ~15 min
After this unit you can
  • Recall the ten categories of the OWASP Top 10:2025, and what changed from the 2021 list
  • Recognise what each category looks like in a TypeScript, React or Node codebase
  • Recall the first fix for each: ownership checks, parameterised queries, a fail-closed default and the rest

Cards

The OWASP Top 10:2025 at a glance

Flag

The OWASP Top 10 is a ranked list of the most critical risk categories for web applications. owasp.org lists the 2025 edition as the most current release:

  1. A01 Broken Access Control
  2. A02 Security Misconfiguration
  3. A03 Software Supply Chain Failures
  4. A04 Cryptographic Failures
  5. A05 Injection
  6. A06 Insecure Design
  7. A07 Authentication Failures
  8. A08 Software or Data Integrity Failures
  9. A09 Security Logging and Alerting Failures
  10. A10 Mishandling of Exceptional Conditions

What changed since 2021: SSRF was rolled into A01; A03 expands 2021's "Vulnerable and Outdated Components" to the whole build and delivery chain; A10 is new; A07, A08 and A09 were renamed; Security Misconfiguration rose from fifth to second.

These are categories, not checklists. Each of the next cards shows one shape the risk takes in a TypeScript, React or Node app.

A01 Broken Access Control

Flag

Users acting outside their permissions: opening another user's record by changing an id (IDOR), calling a write endpoint the UI merely hides, editing a token or hidden field. Since 2025 this category also covers SSRF — a server-side fetch(userSuppliedUrl) that can reach internal services.

// app/api/invoices/[id]/route.ts
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const session = await auth();
  if (!session) return new Response(null, { status: 401 });

  // ❌ db.invoice.findUnique({ where: { id } }) returns anyone's invoice
  const invoice = await db.invoice.findFirst({ where: { id, ownerId: session.userId } });
  if (!invoice) return new Response(null, { status: 404 });
  return Response.json(invoice);
}

OWASP's fixes: enforce access control in trusted server-side code, deny by default, and check ownership on every record a request touches. Logging in proves who someone is, not what they may open.

A02 Security Misconfiguration

Flag

Insecure defaults left in place: verbose errors and stack traces, default accounts, sample apps, storage buckets shared with the internet, missing security headers.

// server.ts — started without NODE_ENV=production
app.get("/orders/:id", async (req, res, next) => {
  try {
    res.json(await getOrder(req.params.id));
  } catch (err) {
    next(err); // Express's default error handler sends err.stack unless NODE_ENV is "production"
  }
});

The code is fine; the deployment isn't. Express's docs: the stack trace "is not included in the production environment", and production mode means NODE_ENV=production. Forget it, and every failing request tells the attacker your file paths and library versions.

OWASP's fixes: an automated, repeatable hardening process that configures every environment the same way, a minimal platform with nothing unused installed, regular review of cloud storage permissions, and security headers on every response (the Security headers worth sending unit).

A03 Software Supply Chain Failures

Flag

Compromises in how your software is built, fetched or updated: a malicious package version, a hijacked maintainer account, an unreviewed CI change. OWASP's example from 2025 is the Shai-Hulud npm worm: malicious versions ran a post-install script that harvested secrets and used the npm tokens it found to publish more poisoned packages. It "reached over 500 package versions before being disrupted".

# CI: install exactly what the reviewed lockfile says, and fail if it's out of date
pnpm install --frozen-lockfile

In a Node project: commit the lockfile and install from it; review new dependencies and remove unused ones; know your transitive tree (an SBOM). Install scripts are the worm's way in: pnpm 10 and npm 12 run a dependency's install scripts only once you allow that package (npm 11 and earlier run them unless you pass --ignore-scripts). Keep publish tokens and CI secrets scoped to the job that needs them.

A04 Cryptographic Failures

Flag

Sensitive data unprotected in transit or at rest, weak or deprecated algorithms (MD5, SHA-1), keys committed to repositories, and randomness that isn't cryptographically secure.

import { randomBytes, scrypt } from "node:crypto";
import { promisify } from "node:util";

const resetToken = randomBytes(32).toString("base64url");   // not Math.random()
const salt = randomBytes(16);
const hash = await promisify(scrypt)(password, salt, 64);   // not sha256(password)

Two classic Node mistakes: a reset token from Math.random() (predictable, not a CSPRNG), and a password stored as a fast hash, which is cheap to brute-force. OWASP names slow, salted, adaptive functions for passwords: Argon2, yescrypt, scrypt, PBKDF2-HMAC-SHA-512 (bcrypt only for legacy systems).

In transit: TLS 1.2 or later everywhere, enforced with HSTS. Keys belong in a secrets manager or the platform's environment, never in the repository.

A05 Injection

Flag

Untrusted input sent to an interpreter as part of a command or query: SQL, NoSQL, OS command, LDAP. XSS is in this category too (the xss unit), as input interpreted by the browser.

// ❌ db.query(`SELECT * FROM users WHERE email = '${email}'`)
const { rows } = await db.query("SELECT * FROM users WHERE email = $1", [email]); // node-postgres

// ❌ exec(`convert ${file} out.png`) — a shell parses `file`
spawn("convert", [file, "out.png"]); // an argument list; no shell involved

OWASP's preferred fix is an API that keeps data out of the interpreter's code entirely: parameterised queries, and argument arrays instead of shell strings. An ORM helps until someone reaches for its raw-query helper with string interpolation. Identifiers (table or column names) can't be parameterised or escaped, so map user choices to an allow-list instead.

LLM prompt injection is related, but OWASP tracks it in a separate list, the LLM Top 10.

A06 Insecure Design

Flag

A missing control, not a coding slip. OWASP: "An insecure design cannot be fixed by a perfect implementation." Its examples include password recovery by security questions, group bookings with no upper limit, and checkouts that bots can sweep.

// ❌ the client decides what it pays
const { productId, quantity, total } = await req.json();
await charge(session.userId, total);

// ✅ the server owns the price and the limits
const product = await db.product.findUniqueOrThrow({ where: { id: productId } });
if (!Number.isInteger(quantity) || quantity < 1 || quantity > product.maxPerOrder) {
  return Response.json({ error: "Invalid quantity" }, { status: 400 });
}
await charge(session.userId, product.price * quantity);

No amount of input validation fixes the first version: the design gave the price to the client.

OWASP's fixes: threat-model the flows that move money or data, add plausibility checks at each tier, and write tests for misuse cases, not just for the happy path.

A07 Authentication Failures

Flag

Login and session handling that attackers can automate or abuse: credential stuffing and brute force that aren't throttled, weak or breached passwords accepted, error messages that reveal which accounts exist, a session id that survives login (session fixation), sessions that outlive logout.

const user = await db.user.findUnique({ where: { email } });
const ok = user !== null && (await verifyPassword(user.passwordHash, password));
if (!ok) {
  await recordFailedLogin(email, ip); // throttle, and alert on spikes
  return Response.json({ error: "Invalid email or password" }, { status: 401 }); // one message for every failure
}
const sessionId = await createSession(user.id); // a fresh id after login, never the pre-login one

OWASP's fixes: MFA, checking new passwords against breached-password lists, limiting or increasingly delaying failed attempts, the same response for every login and recovery outcome, and sessions that end on logout and on idle and absolute timeouts.

A08 Software or Data Integrity Failures

Flag

Trusting code or data without checking where it came from or whether it was changed: unsigned updates, scripts from a CDN with no integrity check, serialised objects an attacker can edit, and cookies used in security decisions without validation.

// ❌ the role comes from a cookie the user can edit in DevTools
const prefs = JSON.parse((await cookies()).get("prefs")?.value ?? "{}");
if (prefs.role === "admin") return <AdminPanel />;

The role belongs in server-side state (the session record or the database), or at least in a value whose signature the server verifies before trusting it. The same idea covers code: a third-party <script> tag can pin the exact file with an integrity (Subresource Integrity) hash.

OWASP's fixes: verify signatures on software and data, consume dependencies only from trusted repositories, never accept unsigned serialised data from clients, and keep CI/CD pipelines segregated, access-controlled and reviewed.

A09 Security Logging and Alerting Failures

Flag

Attacks that go unnoticed: logins, failed logins and high-value transactions that aren't logged; logs nobody alerts on; logs that contain what they mustn't; log data that isn't encoded, so input can forge entries. Renamed from "Monitoring" in 2021 to stress alerting.

// ❌ logger.info({ body: req.body }, "login") writes the password into your logs
logger.warn({ event: "login_failed", accountId: user?.id ?? null, ip }, "login failed");

A structured logger (one JSON object per event) also handles encoding: a newline in user input can't start a fake log line.

OWASP's fixes: log security-relevant successes and failures with enough context to investigate, keep sensitive data (personal data, credentials) out of logs, alert on suspicious patterns such as a burst of failed logins, store an audit trail append-only, and consider honeytokens, decoy records whose use is always an alert.

A10 Mishandling of Exceptional Conditions

Flag

New in 2025: what the code does when something goes wrong. Unhandled exceptions, error messages that leak internals, multi-step operations left half-done, and failing open ("Not Failing Securely", CWE-636).

async function canEdit(userId: string, docId: string): Promise<boolean> {
  try {
    return await policyService.check(userId, "edit", docId);
  } catch {
    return true; // ❌ the policy service timed out, so everyone may edit everything
  }
}

The catch should return false and raise an alert. The same shape hides in config: a missing secret defaulted to "" (the Secrets & config unit).

OWASP's fixes: catch errors where they happen and handle them meaningfully, keep a global handler as a backstop, roll a failed transaction back as a whole ("failing closed"), show users errors that reveal nothing internal, and put limits on resources: "Nothing in information technology should be limitless."

Further reading