← Blog
·11 min read

OWASP Top 10 Explained: Plain English, Real Code Examples (2026)

The OWASP Top 10 explained without jargon. Real code examples, what each vulnerability actually means, and how to check if your app is affected.

Rod

Founder & Developer

The OWASP Top 10 is the most referenced document in web application security. It's also one of the most skimmed — because it's written for security professionals, not the developers who actually need to act on it.

This post translates the OWASP Top 10 explained into language that makes sense for developers building web apps today. For each risk: what it actually means, what it looks like in real code, and what to do about it. No CVSS scores. No enterprise compliance framing. Just the thing, and how to fix it.


A01: Broken Access Control

What it means: Users can access things they shouldn't. Another user's data. Admin pages. Actions they're not authorized to take.

Why it's number one: It's extremely common and the business impact is severe. A broken access control bug on your /api/users/:id endpoint means any logged-in user can read any other user's data by changing the ID in the URL.

// BAD: trusts the client to send the right user ID
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const user = await db.users.findUnique({ where: { id: params.id } });
  return Response.json(user); // anyone can request any user's data
}
 
// GOOD: only returns the authenticated user's own data
export async function GET(req: Request) {
  const session = await getServerSession();
  if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
 
  const user = await db.users.findUnique({ where: { id: session.user.id } });
  return Response.json(user);
}

If you're using Supabase, Row Level Security (RLS) adds a database-level safety net — but only if it's configured correctly. The Supabase RLS security checklist covers the common mistakes.


A02: Cryptographic Failures

What it means: Sensitive data is transmitted or stored without proper encryption. Or it uses encryption that's weak enough to break.

Why it matters: "Cryptographic failure" sounds abstract. In practice it means: passwords stored as plain text (or MD5), credit card numbers in your database without encryption, API responses sent over HTTP instead of HTTPS, or JWT secrets that are actually just the string "secret".

// BAD: weak or missing password hashing
const hashedPassword = md5(password); // MD5 is not a password hash
 
// GOOD: use bcrypt or argon2 with a real cost factor
import bcrypt from "bcrypt";
const hashedPassword = await bcrypt.hash(password, 12);

The HTTPS part is mostly solved by your hosting platform (Vercel, Railway, Netlify all enforce it). The storage and secret-quality parts are on you.


A03: Injection

What it means: Untrusted data is sent to an interpreter as part of a command or query. The classic example is SQL injection — putting user input directly into a database query so an attacker can change the query's meaning.

Why it still appears in 2026: ORMs and query builders largely solve this for database queries. But injection survives in: raw SQL queries, shell command execution, eval(), NoSQL queries built from user input, and template engines used incorrectly.

// BAD: raw SQL with user input directly interpolated
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
// Input: ' OR '1'='1 → returns all users
 
// GOOD: parameterized query (Prisma, Drizzle, or raw pg with $1 syntax)
const user = await prisma.users.findUnique({
  where: { email: userInput }, // Prisma handles escaping
});

SQL injection is a bit less common in modern TypeScript stacks. XSS (Cross-Site Scripting) — a different flavor of injection — is more common and covered in A03's extended scope. See the React security best practices guide for how XSS shows up in React apps specifically.


A04: Insecure Design

What it means: The security problem is in the architecture or design, not the implementation. You can fix the code perfectly and still have a security problem because the design itself is flawed.

A real example: A password reset flow that emails a reset link. If the reset token is the user's email address (or their user ID, or any predictable value), an attacker can construct reset links for any user without receiving any email. The implementation might be flawless — the design is broken.

Another example: an API that lets you look up any order by order number, without verifying that the requesting user owns that order. The bug isn't in the query — it's in the decision to build a lookup-by-ID API without authorization.

This is the OWASP category that code scanners are worst at catching. It requires a human to read the design and ask "who is supposed to be able to do this, and can someone else do it by accident?"


A05: Security Misconfiguration

What it means: Secure software, configured insecurely. Default admin passwords left unchanged. Debug mode enabled in production. Overly permissive CORS settings. S3 buckets set to public. Missing security headers.

What to look for in your own setup:

// BAD: wildcard CORS in production
app.use(cors({ origin: "*" }));
 
// GOOD: explicit allow-list
app.use(cors({ origin: ["https://yourdomain.com"] }));
// BAD: debug mode in production (Next.js example)
// next.config.ts
const config = {
  reactStrictMode: true,
  // if you're logging request bodies in middleware, stop that in production
};

Security headers are a major part of A05. Most developers haven't added them because they're invisible — there's no error if they're missing, just a vulnerability. Check your security headers free to see which ones your deployed app is missing.


A06: Vulnerable and Outdated Components

What it means: Your dependencies have known vulnerabilities. The packages in your node_modules, your Docker base image, your database version — all of these have CVE records that attackers actively scan for.

# Check for known vulnerabilities in your dependencies
npm audit
 
# Fix automatically where possible
npm audit fix

npm audit is a start, but it's not complete. It only checks packages listed in npm's advisory database. Packages compromised through supply chain attacks (a malicious version published by a compromised maintainer) don't always appear there immediately. A dedicated dependency scanner that cross-references the OSV database catches more.

The Veracode 2025 report found that most applications have at least one vulnerable dependency. "Running npm audit and seeing zero results" doesn't mean zero vulnerabilities — it means zero vulnerabilities in npm's database at that moment.


A07: Identification and Authentication Failures

What it means: Broken authentication. Weak password requirements. Missing rate limiting on login endpoints (enabling brute force). Session tokens that don't expire. Password reset flows with predictable tokens.

Common patterns that still appear in 2026:

// BAD: no rate limiting on login — brute force is trivial
export async function POST(req: Request) {
  const { email, password } = await req.json();
  const user = await validateCredentials(email, password);
  // 1000 requests per second? No problem. Bad.
}
 
// GOOD: rate limiting at the edge
// In middleware.ts — limit auth endpoints to 10 req/min per IP

Session fixation — where an attacker sets a known session ID before a user logs in, then uses that session after the user authenticates — is less common in modern auth libraries but still appears in custom implementations.

If you're using an auth library (Auth.js, Clerk, Supabase Auth), most of these are handled for you. The risk area is custom implementations or modifications to default behavior.


A08: Software and Data Integrity Failures

What it means: Code or data is used without verifying its integrity. Classic examples: loading scripts from a CDN without Subresource Integrity (SRI) hashes, processing deserialized data from untrusted sources without validation, or auto-updating software without signature verification.

The supply chain version: An attacker compromises a popular npm package, publishes a malicious version, and your npm install pulls it in because your package.json has a loose version range (^4.2.0 instead of a pinned version).

// More permissive — accepts any 4.x.x version
{
  "dependencies": {
    "some-package": "^4.2.0"
  }
}
 
// More locked-down — use package-lock.json and check it into git
// Then audit the lockfile when it changes

SRI for external scripts:

<!-- BAD: loads whatever is at that URL, no integrity check -->
<script src="https://cdn.example.com/lib.min.js"></script>
 
<!-- GOOD: browsers verify the hash before executing -->
<script
  src="https://cdn.example.com/lib.min.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous"
></script>

A09: Security Logging and Monitoring Failures

What it means: You don't know when you're being attacked. No logs of failed authentication attempts. No alerts on unusual API usage patterns. No audit trail for sensitive operations.

Why this gets deprioritized: Logging feels like infrastructure work, not feature work. It doesn't make the app do anything new. It only matters when something goes wrong — and by then, you need the logs to understand what happened.

What you should be logging at minimum:

  • Failed authentication attempts (with IP, timestamp, email attempted)
  • Successful authentication (user ID, IP, timestamp)
  • Any access to admin endpoints
  • Password change and reset operations
  • Any action that modifies user data

Tools like Sentry capture exceptions but not business logic events. You need structured application logging for the above. Supabase's built-in audit logs help if you're using their auth system.


A10: Server-Side Request Forgery (SSRF)

What it means: Your server makes HTTP requests based on user input, and an attacker can manipulate those requests to reach internal services that should not be publicly accessible.

Classic scenario: Your app has a feature that fetches a URL and displays the content (a screenshot tool, a preview generator, a webhook tester). An attacker submits http://169.254.169.254/latest/meta-data/ — the AWS EC2 metadata endpoint — and your server helpfully fetches the cloud instance's credentials.

// BAD: fetch any URL the user provides, no validation
export async function POST(req: Request) {
  const { url } = await req.json();
  const response = await fetch(url); // attacker submits internal URLs
  return Response.json(await response.json());
}
 
// GOOD: validate the URL is an allowed external host
function isAllowedUrl(url: string): boolean {
  const parsed = new URL(url);
  const blocked = ["169.254.", "10.", "172.16.", "192.168.", "localhost", "127."];
  return parsed.protocol === "https:" &&
    !blocked.some(prefix => parsed.hostname.startsWith(prefix));
}

SSRF became prominent enough that CISA specifically called it out as a critical vulnerability class in recent advisories. Any feature that involves your server fetching user-supplied URLs needs explicit URL validation.


How the OWASP Top 10 Maps to What a Scanner Catches

A good security scanner catches most of A03 (injection), A05 (misconfiguration), A06 (vulnerable components), and parts of A07 (auth failures). It can also detect patterns that suggest A01 (broken access control) and A02 (cryptographic failures).

What scanners struggle with: A04 (insecure design) requires architectural reasoning. A08 (integrity failures) requires analyzing how packages are fetched and verified. A09 (monitoring failures) requires understanding your runtime environment.

This is why a scan is a starting point, not the complete answer. Scan your repo free to get a baseline — see which OWASP categories show up in your findings — then work through the ones scanners can't catch manually.


Frequently Asked Questions

What is the OWASP Top 10?

The OWASP Top 10 is a list of the most critical web application security risks, published by the Open Web Application Security Project. Updated every few years based on real vulnerability data from thousands of applications, it's the baseline document for what web apps should be checked against. It's not a checklist — it's a prioritized set of risk categories.

What is the number one OWASP vulnerability in 2026?

Broken Access Control has been the top OWASP risk since the 2021 edition and remains the leading category going into 2026. It covers situations where users can access resources or perform actions they shouldn't — such as viewing another user's data, accessing admin pages without admin rights, or manipulating API parameters to affect other users' records.

How do I check if my app has OWASP Top 10 vulnerabilities?

Run a security scan against your repository and deployed URL. A good scanner checks for injection risks, broken authentication patterns, security misconfiguration, vulnerable dependencies, and missing security headers — which together cover most of the OWASP Top 10. Data Hogo's free scan covers all of these in under 5 minutes.

What is the difference between OWASP Top 10 and a CVE?

CVEs are specific vulnerability records for specific software — like "this version of lodash has a prototype pollution bug." The OWASP Top 10 are categories of vulnerability classes — like "injection attacks" or "broken access control." A single OWASP category might contain thousands of individual CVEs.

Is the OWASP Top 10 enough to secure my application?

The OWASP Top 10 covers the most common and impactful vulnerability categories, but it's not exhaustive. Passing all OWASP Top 10 checks means you've addressed the most critical risks — not that your application has zero vulnerabilities. Think of it as the essential baseline, not the ceiling.


Scan your repo free →

owaspsecurityvulnerabilitiesweb-securitybest-practices