The OWASP Top 10 is the standard reference for the most critical web application security risks. In 2025, OWASP published a major update to the list. Some categories moved, some were renamed, and two are entirely new: Software Supply Chain Failures and Mishandling of Exceptional Conditions. If you learned the 2017 or 2021 list, this guide brings you up to date. If you are new to security, everything here is explained from the ground up, with plain-language examples and code you can apply today.
What changed in 2025?
The 2025 list reflects how modern attacks actually happen: through dependencies, CI/CD pipelines, and misconfigured cloud services, not just through bad input handling. Here is how the 2021/2017-era categories map to the 2025 list:
| 2017/2021 era | 2025 edition | | --- | --- | | A01 Broken Access Control | A01:2025 Broken Access Control (unchanged, still #1) | | A02 Cryptographic Failures | A04:2025 Cryptographic Failures (moved down) | | A03 Injection | A05:2025 Injection (moved down) | | A04 Insecure Design | A06:2025 Insecure Design | | A05 Security Misconfiguration | A02:2025 Security Misconfiguration (moved up) | | A06 Vulnerable and Outdated Components | A03:2025 Software Supply Chain Failures (expanded and renamed) | | A07 Identification and Authentication Failures | A07:2025 Authentication Failures (renamed) | | A08 Software and Data Integrity Failures | A08:2025 Software or Data Integrity Failures | | A09 Security Logging and Monitoring Failures | A09:2025 Security Logging and Alerting Failures (renamed) | | A10 Server-Side Request Forgery | Folded into other categories; replaced by A10:2025 Mishandling of Exceptional Conditions (new) |
The big story: misconfiguration jumped to number two, supply chain risk got its own expanded category, and SSRF was absorbed into broader categories to make room for a new entry about how applications fail.
A01:2025 Broken Access Control
Broken access control keeps the top spot. It happens when users can act outside their intended permissions: viewing another user's data, modifying records they should not, or escalating themselves to admin.
**A simple example:** an app shows orders at `/api/orders/1234`. If changing the number to `1235` shows someone else's order, access control is broken. The server checked that you were logged in, but never checked that the order belongs to you.
**How to prevent it:** - Deny access by default. Only grant permissions explicitly. - Never rely on client-side checks alone. Always validate on the server. - Check ownership on every request, not just authentication. - Use role-based access control (RBAC) and enforce it at the API layer.
// Bad: trusting the client-supplied user ID
app.get("/api/orders/:userId", (req, res) => {
const orders = db.getOrders(req.params.userId);
return res.json(orders);
});
// Good: use the authenticated session's user ID
app.get("/api/orders", requireAuth, (req, res) => {
const orders = db.getOrders(req.user.id); // from JWT/session
return res.json(orders);
});
A02:2025 Security Misconfiguration
Security misconfiguration jumped from fifth to second place because modern apps have so many moving parts: cloud buckets, containers, serverless functions, CDNs, and frameworks. Every one of them ships with defaults, and defaults are rarely secure.
**Common examples:** default admin credentials left enabled, an S3 bucket set to public, overly permissive CORS (`Access-Control-Allow-Origin: *` on an authenticated API), verbose stack traces shown to users in production, and missing security headers.
**How to prevent it:** - Automate your security configuration. Infrastructure as code makes settings reviewable and repeatable. - Disable default accounts, sample apps, and unused features. - Set proper security headers: Content-Security-Policy, X-Frame-Options, Strict-Transport-Security. - Review cloud permissions regularly. Least privilege applies to infrastructure too.
// Express security headers with helmet
import helmet from "helmet";
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
}));
// Disable X-Powered-By header
app.disable("x-powered-by");
A03:2025 Software Supply Chain Failures
This is the expanded successor to Vulnerable and Outdated Components, and it is the biggest conceptual change in the 2025 list. The old category covered using a library with a known vulnerability. The new one covers the entire supply chain: your dependencies, your build system, your CI/CD pipeline, and the updates you install.
**Why the change?** Because attackers stopped waiting for you to use a vulnerable library and started attacking the supply chain itself. The xz Utils backdoor (2024) showed a maintainer account being compromised to ship malicious code in a trusted package. The Log4Shell vulnerability (CVE-2021-44228) showed how one outdated dependency can compromise entire systems.
**How to prevent it:** - Run `npm audit` regularly (or your package manager's equivalent) and act on the results. - Use tools like Dependabot or Snyk for automated vulnerability scanning. - Pin dependencies with lock files and review what changes in every update. - Remove unused dependencies. Every dependency is attack surface. - Verify the integrity of what you install: signatures, checksums, and provenance where available.
A04:2025 Cryptographic Failures
Formerly called Sensitive Data Exposure, this covers failures related to cryptography that lead to exposure of sensitive data: passwords stored in plain text, data transmitted over HTTP, weak hashing algorithms, or hardcoded keys in source code.
**A simple test:** if your database leaked tomorrow, what would attackers read? If the answer includes plain-text passwords, unencrypted card numbers, or API keys, you have a cryptographic failure.
**How to prevent it:** - Always use HTTPS. No exceptions. - Hash passwords with bcrypt, scrypt, or Argon2. Never MD5 or SHA-1. - Encrypt sensitive data at rest using strong algorithms (AES-256). - Keep secrets in a secrets manager or environment variables, never in git. - Do not store sensitive data you do not need.
A05:2025 Injection
SQL injection, NoSQL injection, command injection, LDAP injection, and now prompt injection for AI-powered features. Any time user input is sent to an interpreter as part of a command or query without proper handling, you have an injection vulnerability.
**A simple example:** a login form that builds a SQL query by gluing strings together. An attacker types `' OR '1'='1` as the email, and the query returns every user in the database.
**How to prevent it:** - Use parameterized queries or prepared statements. Always. - Validate and sanitize all user input. - Use ORMs, but understand their limitations. - For AI features, treat user input as untrusted instructions and separate it from system prompts.
// Bad: string concatenation in SQL (injection risk)
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Good: parameterised query
const query = "SELECT * FROM users WHERE email = $1";
const result = await db.query(query, [email]);
// Good: using an ORM with built-in protection
const user = await prisma.user.findUnique({
where: { email },
});
A06:2025 Insecure Design
Insecure design is about fundamental design flaws, not implementation bugs. No amount of perfect code can fix a flawed design. Examples include missing rate limiting on authentication, not considering abuse scenarios, or letting a password reset flow reveal whether an email address is registered.
**The difference from a bug:** a bug is a mistake in the code. A design flaw is a mistake in the plan. If your design says 'users can reset their password with just their email', an attacker can lock out any account by requesting resets in a loop. The code can be perfect and the design is still broken.
**How to prevent it:** - Threat model your application before building. Ask: how would I abuse this feature? - Use secure design patterns: defense in depth, least privilege, fail securely. - Write abuse cases alongside your user stories.
A07:2025 Authentication Failures
Previously called Identification and Authentication Failures, this covers weak passwords, missing brute-force protection, session tokens in URLs, and sessions that stay valid after logout. Authentication is genuinely difficult to implement correctly, which is why you should not build it yourself.
**How to prevent it:** - Implement multi-factor authentication. - Never ship with default credentials. - Rate-limit login attempts and add exponential backoff. - Invalidate sessions properly on logout and on password change. - Use established authentication libraries or managed auth providers instead of rolling your own.
A08:2025 Software or Data Integrity Failures
This covers assumptions about software updates, critical data, and CI/CD pipelines without verifying integrity. Think unsigned updates, deserializing untrusted data, or a build pipeline that anyone can push to.
**A simple example:** if your app auto-updates by downloading a file from a server and running it, what stops an attacker who compromises that server from shipping malware to every user? Without signature verification, nothing.
**How to prevent it:** - Verify digital signatures on software and updates. - Use lock files (package-lock.json, yarn.lock) and review dependency changes. - Protect your CI/CD pipeline: require reviews, restrict who can modify build configs, sign artifacts. - Do not deserialize untrusted data without validation.
A09:2025 Security Logging and Alerting Failures
Renamed from Logging and Monitoring Failures to put the emphasis where it belongs: alerting. Logs nobody reads are just disk usage. If you cannot detect a breach, you cannot respond to it, and many organizations discover breaches months after they happen.
**How to prevent it:** - Log all authentication events (successes and failures). - Log access control failures and input validation failures. - Ensure logs are not vulnerable to injection themselves (attackers will try to forge log entries). - Set up alerts for suspicious patterns: multiple failed logins, unusual access volume, requests from unexpected locations. - Test your alerts. An alert that has never fired is an alert you cannot trust.
A10:2025 Mishandling of Exceptional Conditions
This is the brand-new category, replacing SSRF. It covers how applications behave when things go wrong: unhandled exceptions, error messages that leak internals, and code that fails open instead of failing closed.
**A simple example:** a payment flow where an exception in the fraud check lets the payment proceed anyway. The system failed open: when the safety check crashed, the default behavior was 'allow'. Secure systems fail closed: when in doubt, deny.
**Common mistakes:** - Showing stack traces or database errors to users (leaks internal structure to attackers). - Catch blocks that swallow errors and let execution continue in an unknown state. - Timeouts and error paths that skip authorization checks.
**How to prevent it:** - Fail closed. When a security check errors out, deny the action. - Show users generic error messages; log the details server-side. - Handle exceptions at every boundary, and never let an error bypass a security control.
// Bad: failing open - if the fraud check throws, the payment proceeds
async function processPayment(payment: Payment) {
try {
await fraudCheck(payment);
} catch (err) {
console.error("fraud check failed", err);
}
return chargeCard(payment); // runs even when the check failed
}
// Good: failing closed - an error in the check blocks the payment
async function processPayment(payment: Payment) {
try {
await fraudCheck(payment);
} catch (err) {
logger.error("fraud check failed", { err, paymentId: payment.id });
throw new PaymentRejectedError("Payment could not be verified");
}
return chargeCard(payment);
}
Where did SSRF go?
Server-Side Request Forgery did not stop mattering. In the 2025 list it was folded into broader categories rather than keeping its own slot, because the data showed it overlapping heavily with access control and configuration issues. The defenses are unchanged: validate user-supplied URLs, use allowlists for permitted domains, block requests to internal IP ranges (127.0.0.1, 10.x, 169.254.x), and never expose raw responses from server-side fetches to users.
Key Takeaways
- The OWASP Top 10 was updated in 2025: misconfiguration rose to #2, supply chain failures got an expanded category, and Mishandling of Exceptional Conditions is brand new - Broken access control is still the number one risk: check ownership on every request, not just login state - Your dependencies, build pipeline, and updates are part of your attack surface: pin, audit, and verify them - Use parameterized queries to prevent injection, never string concatenation - Hash passwords with bcrypt, scrypt, or Argon2 and always use HTTPS - Fail closed: when a security check errors, deny the action - Log security events and, more importantly, alert on them - Security is not a feature you add at the end: it is a mindset for every line of code