Software / Security

OWASP Top 10 for Web Developers: A Practical Security Checklist

/15 min read

The OWASP Top 10 is the industry-standard awareness document for the most critical web application security risks, most recently in its 2021 edition with ten risk categories ranked by exploitability, prevalence, and impact. For a working web developer it is not a reading list — it is a checklist where each category maps to specific code, configuration, and architecture changes you can make this sprint.

How to Use This Guide

Each of the ten sections below follows the same shape: what the risk actually is, a real-world example of how it gets exploited, and the concrete fix — including code where the fix is a code change. At the end you'll find a consolidated table of risk → fix → test method, the security headers checklist, and the tooling (ZAP, Burp, Semgrep, Snyk) to automate most of the verification.

A note on editions: the current official list is OWASP Top 10:2021. Its categories are numbered A01 through A10, and the 2021 rewrite notably merged the old Injection category almost entirely into A03 while elevating Insecure Design (A04) to a category of its own — an explicit acknowledgment that most breaches are design flaws, not single-line bugs. OWASP continues to publish supplemental guidance (API Security Top 10, LLM Application risks, ASVS verification standard) alongside it.

A01:2021 — Broken Access Control

What it is: Users acting outside their intended permissions: reading or editing other users' records, accessing admin functions, or bypassing enforced checks entirely. OWASP ranks it the most impactful category year after year.

Real-world example: An API exposes GET /api/invoices/:id and authenticates the caller but never verifies the invoice belongs to them. Attacker enumerates sequential IDs and harvests every customer's invoices — the classic IDOR (Insecure Direct Object Reference). Roughly a third of applications tested in large bug-bounty programs have at least one instance of it.

The fix: enforce authorization at the object level on every request, deny by default, and never trust identifiers supplied by the client. Centralize the check so it cannot be forgotten per-route:

// VULNERABLE — authenticates, but never checks ownership
app.get("/api/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await db.invoices.find(req.params.id);
  res.json(invoice);
});

// SAFE — object-level authorization in the query itself
app.get("/api/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await db.invoices.find({
    id: req.params.id,
    ownerId: req.user.id,          // deny by default: no row, no data
  });
  if (!invoice) return res.status(404).end();
  res.json(invoice);
});

Beyond IDOR: disable directory listing, enforce record-level scoping in your ORM queries instead of find-by-id followed by a manual ownership check, deny CORS origins you do not recognize, and log access-control failures so you can see probing. The deny by default rule means new routes ship closed and must explicitly declare what they allow.

A02:2021 — Cryptographic Failures

What it is: Sensitive data exposed because of missing or broken encryption: plaintext passwords in the database, HTTP where HTTPS should be, weak or homegrown algorithms, keys in source control.

Real-world example: A breach dump shows 8 million rows with MD5 password hashes. MD5 is fast enough to brute-force at billions of guesses per second on a GPU, and username/email pairs in the same dump feed credential-stuffing attacks on every other site where users reused the password.

The fix: use a dedicated password hashing algorithm — bcrypt (cost 12+), Argon2id, or scrypt — never MD5, SHA-1, or plain SHA-256 for passwords. TLS everywhere, HSTS enabled, encryption at rest for PII, and no custom crypto ever:

import bcrypt from "bcrypt";

// cost factor 12+ ; bcrypt self-limits to 72 bytes
// (truncate or hash longer secrets separately)
const hash = await bcrypt.hash(plaintextPassword, 12);

const ok = await bcrypt.compare(attemptedPassword, storedHash);

// New projects: Argon2id is the modern default
import { argon2id } from "argon2";
const hash2 = await argon2.hash(plaintextPassword, { type: argon2id });

Classify your data (what is PII, what is a secret, what is public), then map each class to a control. Rotate keys, store them in a KMS or secrets manager rather than environment files committed to git, and disable weak TLS versions and CBC-mode cipher suites at the load balancer.

A03:2021 — Injection

What it is: Untrusted input interpreted as command, query, or code: SQL injection, OS command injection, template injection, LDAP injection. The 2021 edition folded Cross-Site Scripting into this category's scope.

Real-world example: The 2019 breach of a major hotel group traces to an SQL injection in a booking API — attacker-controlled input flowed into a concatenated query, and hundreds of millions of guest records followed. Injection remains a fixture of every major breach retrospective because the vulnerable pattern is one line of code.

The fix: parameterized queries and prepared statements as the default — your database driver makes user input incapable of changing query structure. Use an ORM correctly (raw query builders with bindings are fine; string interpolation is not). Additional layers: validate and type-coerce input at the boundary with a schema library (Zod, Joi, Pydantic), use spawn with argument arrays instead of shell strings for any process execution, escape output for the correct context, and deploy a Content Security Policy so a successful XSS payload cannot exfiltrate or execute freely.

A04:2021 — Insecure Design

What it is: Flaws baked into the design itself, not implementable bugs: missing rate limits, no abuse-case thinking, business logic that allows negative quantities, password-reset flows that leak account existence.

Real-world example: A food-delivery promo applies "first order free" per account — attackers script account creation with disposable emails and redeem it thousands of times. No single line of code is buggy; the design never modeled the abuse case.

The fix: threat-model before building. A lightweight session — draw the data flow, list abuse cases per feature, pick controls — catches entire classes of bugs. Concretely: rate-limit sensitive endpoints (login, signup, password reset, search) per IP and per account, enforce invariants at the domain layer (quantity > 0, total matches line items, state transitions are legal), use CAPTCHA or device signals on high-abuse flows, and design multi-tenant isolation into the data model rather than bolting it on. The OWASP Application Security Verification Standard (ASVS) turns design requirements into checkable statements.

A05:2021 — Security Misconfiguration

What it is: Default credentials left in place, verbose stack traces in production, permissive CORS (Access-Control-Allow-Origin: * with credentials), unnecessary cloud storage buckets public, debug endpoints exposed, frameworks left on default configs.

Real-world example: Cloud-bucket scanning is automated and continuous: any S3 bucket, Blob container, or GCS bucket accidentally made public is found within days. Entire categories of 2020s breaches trace to a single misconfigured storage or admin panel left on defaults.

The fix: one hardened, version-controlled configuration applied identically across environments (infrastructure as code, not click-ops). Disable debug mode and stack traces behind a generic error page; set CORS to an explicit origin allowlist; remove default accounts and unused endpoints; run periodic misconfiguration scans against your own infrastructure (ScoutSuite, Prowler for cloud, or ZAP's baseline scan for the web tier). Secrets in the environment or a vault — never in the image.

A06:2021 — Vulnerable and Outdated Components

What it is: Running a component with a known CVE — a vulnerable npm package, an EOL Python version, an unpatched Log4j-style library deep in the dependency tree. Prevalent because scanning and patching are unglamorous work.

Real-world example: The 2021 Log4Shell (CVE-2021-44228) exposed a remote-code-execution bug in a logging library buried in thousands of Java applications — including systems where no developer had ever heard of Log4j. Transitive dependencies are the attack surface you do not see.

The fix: automated Software Composition Analysis (SCA) in CI. Enable Dependabot or Renovate for continuous update PRs, run Snyk or Trivy in the pipeline to fail on critical advisories, keep a software bill of materials (SBOM) per release, and delete dependencies you no longer use. Pin versions with lockfiles, and audit transitive deps — most exploited vulnerabilities live two levels below your direct imports.

A07:2021 — Identification and Authentication Failures

What it is: Weak login mechanisms: no MFA, unthrottled login attempts, weak password policies, session IDs that survive password change, missing session invalidation, allowing default or breached passwords.

Real-world example: Credential stuffing — billions of username/password pairs from unrelated breaches replayed against your login — is fully automated. Without rate limiting and breached-password checks, a site with 0.1% credential reuse loses thousands of accounts per campaign.

The fix: offer MFA (TOTP or passkeys — WebAuthn passkeys are phishing-resistant by design), rate-limit and add progressive delays on login and password reset per IP and per account, check new passwords against a breached-password corpus (Have I Been Pwned k-anonymity API or a local bloom filter), and rotate session IDs on login and privilege change. Sessions must die on password change and logout everywhere. Use established auth libraries and identity providers — hand-rolled session code is where these failures breed.

A08:2021 — Software and Data Integrity Failures

What it is: Trusting code and data without verifying integrity: insecure CI/CD pipelines, auto-update mechanisms without signature checks, unsigned webhooks, unsafe deserialization of untrusted objects, and — increasingly — dependency confusion and tampered build artifacts.

Real-world example: The 2020 SolarWinds attack succeeded by injecting malicious code into a signed software build pipeline — customers installed updates that were authentic, signed, and compromised. The same class of risk applies to any pipeline whose credentials can publish artifacts.

The fix: verify webhook signatures with constant-time comparison before parsing the body; never deserialize untrusted data into arbitrary objects (use JSON with strict schemas, or language formats with no code-execution semantics); pin CI actions by commit SHA and require signed commits; adopt SLSA-style provenance so consumers can verify where an artifact was built; and protect your pipeline with the same least-privilege rules as production access. Webhook verification in Node looks like this:

import crypto from "node:crypto";

function verifySignature(rawBody, header, secret) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

  // constant-time compare — never === on signatures
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Verify BEFORE parsing JSON, and reject if the timestamp is stale.

A09:2021 — Security Logging and Monitoring Failures

What it is: Breaches that go undetected for months because failed logins, access-control denials, and admin actions are not logged, not alerted on, or not retained. OWASP notes the average breach detection time is measured in months when this category fails.

Real-world example: An attacker with valid credentials performs quiet enumeration for eight weeks. No alert fires on 200 failed authorization attempts per day because the app logs only 500-level errors. The breach is discovered by a third party, not by the team.

The fix: log security-relevant events — login success and failure, permission denies, admin actions, input validation failures, password resets — with user ID, timestamp, source IP, and event type. Never log secrets, session tokens, or full payment data. Centralize logs (ELK, Loki, Datadog, CloudWatch), then alert on patterns: burst failures on one account, unusual admin hours, spikes in 403s. Rehearse response: a runbook without practice is a wish.

A10:2021 — Server-Side Request Forgery (SSRF)

What it is: An attacker makes your server issue HTTP requests to destinations the attacker chooses — internal services, cloud metadata endpoints, port-scan your VPC from the inside.

Real-world example: A "fetch this URL" webhook-preview feature is pointed at http://169.254.169.254/latest/meta-data/iam/security-credentials/, the cloud instance metadata service. The server dutifully returns the instance's IAM credentials to the attacker. SSRF became a headline category precisely because of cloud metadata exposure.

The fix: allowlist outbound destinations wherever possible instead of deny-listing. Resolve hostnames and reject private, loopback, and link-local ranges (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, ::1, fc00::/7) after DNS resolution — not before, to defeat DNS rebinding. Disable redirects, or re-validate every hop. Block port ranges beyond 80/443. And on modern cloud platforms, require IMDSv2 (token-required instance metadata) so even a successful SSRF cannot read credentials without the PUT token step. Network-level egress policies for the application tier are the last line and the most reliable one.

Security Headers Checklist

Headers are the cheapest security control you can ship: a few lines of configuration that harden every response. Set them centrally at the edge or in middleware, then verify with securityheaders.com or the browser devtools Network panel.

# Next.js middleware / edge config — equivalent in any stack
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY          # legacy fallback for frame-ancestors

The load-bearing one is Content-Security-Policy — it is your safety net against XSS and injected scripts. Start in report-only mode, watch the reports, then enforce. frame-ancestors 'none' (with X-Frame-Options as legacy fallback) kills clickjacking, Strict-Transport-Security forces HTTPS for two years once preloaded, and Referrer-Policy: strict-origin-when-cross-origin stops full URLs (which can contain IDs and tokens) leaking to third parties.

OWASP Top 10 at a Glance: Risk, Fix, Test

One table, the whole checklist — use it as the spine of a quarterly security review:

RiskPrimary fixTest method
A01 Broken Access ControlObject-level authz, deny by defaultIDOR tests per resource; ZAP auth scan
A02 Cryptographic FailuresArgon2id/bcrypt, TLS + HSTSssllabs.com scan; hash-format audit
A03 InjectionParameterized queries, CSPZAP active scan; sqlmap on test env
A04 Insecure DesignThreat model, rate limits, abuse casesDesign review; abuse-case test suite
A05 MisconfigurationHardened IaC baseline, no debug in prodZAP baseline scan; ScoutSuite/Prowler
A06 Vulnerable ComponentsSCA in CI, Dependabot/RenovateSnyk/Trivy CI gate; SBOM per release
A07 Auth FailuresMFA, throttling, breached-pw checksCredential-stuffing simulation; session tests
A08 Integrity FailuresSigned webhooks, pinned CI, no unsafe deserializeSignature-bypass tests; SLSA provenance check
A09 Logging FailuresSecurity event logs + alertsAlert-fire drill; log coverage audit
A10 SSRFDestination allowlist, IMDSv2, egress policyMetadata-endpoint probe; internal-range tests

Testing Tools: Automate 70% of This List

Manual review finds design flaws; scanners find mechanical ones. A pragmatic stack covers both:

  • OWASP ZAP: Free, open-source. Run the baseline scan on every deploy for headers and quick wins, the full active scan nightly against staging with an authenticated context, and the API scan if you publish OpenAPI specs.
  • Burp Suite: The professional standard for manual and semi-automated penetration testing — proxy every request through Burp in a test pass, crawl with the Detective, and let experienced testers chase logic flaws scanners cannot model.
  • Semgrep: Pattern-based static analysis that catches injection sinks, unsafe deserialization, and hard-coded secrets in your own code. Runs in CI in seconds and writes rules that match your framework.
  • Snyk / Trivy: Dependency and container scanning for A06. Wire the critical-advisory gate into CI; triage the rest on a schedule.
  • ASVS: Not a tool but a standard: the OWASP Application Security Verification Standard gives you verifiable requirements (Level 1 for any app, Level 2 for those holding personal data) to turn this checklist into an acceptance suite.

Conclusion

The OWASP Top 10 rewards teams that treat security as defaults rather than heroics: parameterized queries instead of careful string building, Argon2 instead of a hashing choice made once and forgotten, object-level authorization enforced by a shared helper instead of per-route discipline, scanners in CI instead of annual pen tests. Nine of the ten categories are prevented or dramatically reduced by defaults your framework can enforce for you — the tenth, Insecure Design, is prevented by asking "how would someone abuse this?” before writing code.

Run the checklist table quarterly, keep ZAP and Snyk gating your pipeline, and read one breach retrospective a month. That rhythm keeps the ten risks boring — which is exactly what security should feel like from the inside.

Key Takeaways

  • A01 Broken Access Control is the highest-impact category: enforce authorization on every object, deny by default, and never trust client-supplied IDs.
  • Hash passwords with Argon2id, bcrypt (cost 12+), or scrypt — never MD5 or unsalted SHA; TLS and HSTS everywhere, no homegrown crypto.
  • Parameterized queries plus a strict input schema and a real CSP neutralize injection and XSS at three different layers.
  • Insecure Design (A04) cannot be scanned for — threat-model each feature and test abuse cases; rate-limit and enforce invariants in the domain layer.
  • Automate the mechanical half: ZAP baseline scans, Semgrep rules, and Snyk/Dependabot in CI, with ASVS as the verification standard.

FAQ

What is the OWASP Top 10?

The OWASP Top 10 is a regularly updated awareness document from the Open Worldwide Application Security Project listing the ten most critical web application security risks. The current edition is 2021 (A01–A10), ranked by prevalence, exploitability, and technical impact, and it serves as the de-facto baseline for secure development training and audit frameworks.

Is there a 2025 edition of the OWASP Top 10?

OWASP runs a continuous, data-driven process for each new edition, with community submissions and working drafts published on owasp.org. Check owasp.org/Top10 for the current release status; regardless of edition, the 2021 categories remain the practical checklist, and supplemental lists (API Security, LLM Applications) cover surfaces the main list does not.

Do frameworks like Next.js or Rails prevent the OWASP Top 10?

Modern frameworks auto-escape output (helping with XSS), provide ORM query builders (helping with injection), and ship CSRF protections by default. They do not, however, implement object-level authorization, secure your CORS config, rate-limit your endpoints, or fix insecure design — those remain your responsibility, and that is where most real findings live.

How often should I run OWASP Top 10 checks?

Continuously, in layers: dependency and SAST scanning on every pull request, ZAP baseline scans on every deploy, full authenticated ZAP or Burp testing against staging monthly or quarterly, and a formal ASVS-aligned review annually or before any major release. Automated gates catch regressions; human testing catches design flaws.

What is the single most important OWASP fix?

Object-level authorization on every request (A01). Broken access control is consistently the most impactful category because it converts any other bug — an exposed ID, a predictable URL, a leaked token — into direct data compromise. Pair it with parameterized queries and MFA, and you have covered the majority of real-world breach paths.

References

  1. OWASP. "Top Ten — 2021." Open Worldwide Application Security Project. owasp.org/Top10
  2. OWASP. "Cheat Sheet Series." cheatsheetseries.owasp.org
  3. OWASP. "Application Security Verification Standard (ASVS)." owasp.org/www-project-application-security-verification-standard
  4. MITRE. "Common Weakness Enumeration (CWE) Top 25." cwe.mitre.org/top25
  5. PortSwigger. "Web Security Academy." Free hands-on labs for injection, access control, and SSRF. portswigger.net/web-security
  6. OWASP. "Zed Attack Proxy (ZAP)." zaproxy.org
Summarize with AI
Page