The OWASP Top 10 is the most widely referenced list of web application security risks, and the OWASP Top 10 2025 is its eighth edition. It was presented in November 2025 at OWASP Global AppSec and is published at top10.owasp.org. According to OWASP, this edition analyzed 589 CWEs with data contributed from more than 2.8 million applications, and two of the ten categories were chosen from a community survey rather than raw data.
This guide goes through all ten categories. For each one you get what it means in practice, a short vulnerable snippet, the fixed version, and how you would actually detect it in your own codebase.
The OWASP Top 10 2025 list
| 2025 | Category | Change vs 2021 |
|---|---|---|
| A01 | Broken Access Control | Still #1, now includes SSRF |
| A02 | Security Misconfiguration | Up from #5 |
| A03 | Software Supply Chain Failures | New, expands Vulnerable and Outdated Components |
| A04 | Cryptographic Failures | Down from #2 |
| A05 | Injection | Down from #3 |
| A06 | Insecure Design | Down from #4 |
| A07 | Authentication Failures | Renamed from Identification and Authentication Failures |
| A08 | Software or Data Integrity Failures | Same position |
| A09 | Security Logging and Alerting Failures | Renamed, emphasis on alerting |
| A10 | Mishandling of Exceptional Conditions | New |
The big story of 2025: the risk has shifted from "my code has a bug" toward "my configuration, my dependencies and my pipeline have a bug". Two of the top three categories are things a traditional code review barely looks at.
A01: Broken Access Control
Users can do or see things they should not: read another customer's invoice, call an admin endpoint, or change an ID in the URL and get someone else's data (IDOR). In 2025 OWASP also folded Server-Side Request Forgery (SSRF) into this category, since it is ultimately the server accessing a resource it should not.
// Vulnerable: any logged-in user can read any invoice
app.get("/api/invoices/:id", auth, async (req, res) => {
const invoice = await Invoice.findById(req.params.id);
res.json(invoice);
});
// Fixed: scope the query to the current user
app.get("/api/invoices/:id", auth, async (req, res) => {
const invoice = await Invoice.findOne({ _id: req.params.id, userId: req.user.id });
if (!invoice) return res.status(404).end();
res.json(invoice);
});How to detect it: write tests that log in as user A and request user B's resources. Scanners that only match patterns struggle here because the bug is a missing check, so you need tools that understand the data flow between the request, the session and the query, plus manual or AI pentest passes over sensitive endpoints.
A02: Security Misconfiguration
Debug mode in production, default credentials, overly permissive CORS, verbose error pages, public storage buckets. It jumped to #2 because modern apps are mostly configuration: cloud resources, containers, CI files and framework settings.
# Vulnerable (Terraform): a bucket anyone on the internet can read
resource "aws_s3_bucket_acl" "reports" {
bucket = aws_s3_bucket.reports.id
acl = "public-read"
}
# Fixed: keep it private and block public access explicitly
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}How to detect it: Infrastructure as Code scanning on every pull request (Terraform, Kubernetes manifests, Dockerfiles, GitHub Actions workflows), plus a baseline DAST scan to catch headers and error pages in the running app.
A03: Software Supply Chain Failures
The new category that replaces and expands "Vulnerable and Outdated Components". It covers the whole chain: dependencies with known CVEs, malicious or typosquatted packages, compromised build pipelines, and third-party CI actions you run with access to your secrets.
# Vulnerable: mutable references, anything can change under you
"dependencies": { "some-lib": "*" }
- uses: some-org/deploy-action@main
# Fixed: lockfile + exact versions, actions pinned to a commit SHA
"dependencies": { "some-lib": "4.2.1" } # and commit package-lock.json, install with npm ci
- uses: some-org/deploy-action@3f1c2a9e8b7d6c5a4f3e2d1c0b9a8f7e6d5c4b3aHow to detect it:Software Composition Analysis (SCA) checks your lockfiles against known CVE databases. Add it to CI so a vulnerable version cannot be merged silently, and review workflow files for unpinned actions and overly broad permissions.
A04: Cryptographic Failures
Sensitive data exposed because of weak or missing cryptography: passwords hashed with MD5 or SHA-1, data sent over plain HTTP, hardcoded keys, predictable random numbers for tokens.
# Vulnerable (Python): fast, unsalted hash, trivial to crack
import hashlib
stored = hashlib.md5(password.encode()).hexdigest()
# Fixed: a slow, salted password hashing algorithm
from argon2 import PasswordHasher
ph = PasswordHasher()
stored = ph.hash(password)
ph.verify(stored, attempt) # raises on mismatchHow to detect it: SAST catches weak algorithms, Math.random() used for tokens and hardcoded keys. A secret scanner finds keys that were committed, including ones deleted later but still present in git history.
A05: Injection
Untrusted input interpreted as code or query: SQL injection, NoSQL injection, OS command injection, and cross-site scripting (XSS), which OWASP includes here. We cover the SQL case in depth in our SQL injection guide.
# Vulnerable: user input concatenated into SQL
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
# Fixed: parameterized query, the driver handles escaping
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))How to detect it: this is where SAST shines, since it can trace input from a request parameter to a dangerous sink. DAST confirms it from outside. See SAST vs DAST for when to use each.
A06: Insecure Design
Flaws in the logic itself, not in the implementation. The code does exactly what it was designed to do, and the design is exploitable. Classic example: a password reset with a short numeric code and no attempt limit.
# Vulnerable design: 4-digit code, no expiry, unlimited attempts
code = random.randint(1000, 9999)
if request.form["code"] == str(user.reset_code):
allow_reset(user)
# Fixed design: long random token, expiry, attempt limit
token = secrets.token_urlsafe(32)
user.reset_token_hash = sha256(token)
user.reset_expires_at = now() + timedelta(minutes=15)
# on verify: check expiry, compare hashes in constant time, lock after 5 failuresHow to detect it: no scanner reliably finds design flaws from syntax alone. Threat modeling before building, security review of flows like signup, reset and checkout, and pentesting (see what is penetration testing) are the realistic defenses.
A07: Authentication Failures
Weak login and session handling: credential stuffing without rate limits, weak password rules, session IDs that never expire, and JWTs that are decoded without proper verification.
# Vulnerable (PyJWT): signature is never checked
payload = jwt.decode(token, options={"verify_signature": False})
# Fixed: verify signature and pin the allowed algorithm
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])How to detect it: SAST flags disabled verification and weak session settings. Test login endpoints for rate limiting and account enumeration (different error messages for "user not found" vs "wrong password").
A08: Software or Data Integrity Failures
Trusting code or data without verifying its integrity: insecure deserialization, auto-updates without signature checks, loading plugins or scripts from untrusted sources.
# Vulnerable: deserializing attacker-controlled bytes can execute code
data = pickle.loads(request.data)
# Fixed: use a data-only format and validate the shape
data = json.loads(request.data)
order = OrderSchema().load(data) # rejects unexpected fields and typesHow to detect it: SAST detects dangerous deserializers (pickle, Java ObjectInputStream, unsafe YAML loaders). In CI, verify checksums or signatures of artifacts you download.
A09: Security Logging and Alerting Failures
If you cannot see an attack, you cannot respond to it. The 2025 name stresses alerting: logs nobody reads do not count. The opposite mistake is also common: logging secrets.
# Vulnerable: logs the password, and failed logins are not tracked
logger.info(f"login attempt {email} {password}")
# Fixed: structured event, no secrets, feeds an alert rule
logger.warning("auth.login_failed", extra={"email": email, "ip": ip})
# alert: more than 20 auth.login_failed from one IP in 5 minutesHow to detect it: review which security events are logged (logins, permission denials, admin actions), grep for secrets in log statements, and actually trigger an alert in staging to prove it fires.
A10: Mishandling of Exceptional Conditions
New in 2025. Programs that fail to prevent, detect or respond to unusual situations: uncaught exceptions, error messages leaking internals, partial transactions, and the worst one, failing open.
# Vulnerable: if the permission service is down, everyone is allowed
try:
allowed = permissions.check(user, "delete_project")
except Exception:
allowed = True
# Fixed: fail closed and log it
try:
allowed = permissions.check(user, "delete_project")
except Exception:
logger.exception("permission check failed")
allowed = FalseHow to detect it: look for broad except/catch blocks that change security decisions, stack traces returned to clients, and missing rollbacks. Fault injection tests (make a dependency time out and see what happens) are very effective.
OWASP API Security Top 10
If what you ship is mostly an API, also check the OWASP API Security Top 10. Its latest edition is from 2023 and focuses on API-specific risks:
- API1: Broken Object Level Authorization (BOLA)
- API2: Broken Authentication
- API3: Broken Object Property Level Authorization
- API4: Unrestricted Resource Consumption
- API5: Broken Function Level Authorization
- API6: Unrestricted Access to Sensitive Business Flows
- API7: Server Side Request Forgery
- API8: Security Misconfiguration
- API9: Improper Inventory Management
- API10: Unsafe Consumption of APIs
Notice how authorization appears three times. In APIs, access control bugs are by far the most common serious finding, which lines up with Broken Access Control staying at #1 in the main OWASP Top 10.
How to cover the OWASP Top 10 in practice
| Technique | Best at |
|---|---|
| SAST | A01, A04, A05, A07, A08, A10 in source code |
| SCA | A03 dependencies with known CVEs |
| Secret scanning | A04 leaked keys and tokens |
| IaC scanning | A02 cloud, container and CI misconfigurations |
| DAST | A02, A05, A07 from outside, in a running app |
| Pentest / threat modeling | A01 and A06 business logic and design |
The practical answer is to run the automated parts on every change, which is what DevSecOps is about, and reserve human time for design and logic. A vulnerability scanner that reads your repo covers most of the table in one pass.
For example, with Nurbak you connect GitHub and scan a repository: Nurbak's own self-hosted AI model analyzes the code (it is not sent to OpenAI or Anthropic for analysis), reports exploitable vulnerabilities with file and line, checks dependencies against known CVEs, flags GitHub Actions, Docker, Terraform and Kubernetes misconfigurations, and finds secrets in git history. The free scan shows the 3 most important findings in full, so you can see where your repo stands against the OWASP Top 10 before deciding anything.
Key takeaways
- The OWASP Top 10 2025 is led by Broken Access Control, Security Misconfiguration and Software Supply Chain Failures.
- SSRF is now part of A01, and A10 (Mishandling of Exceptional Conditions) is new: never fail open.
- Every category needs a different detection technique; no single tool covers all ten.
- If you ship APIs, check the OWASP API Security Top 10 too.
- Start with an automated baseline: scan your repository and fix the highest-impact findings first.
