Home › Security › Application security
guide low

OWASP Top 10 risks

Problem Teams designing, reviewing or testing a web app or API need to know which vulnerability classes cause most real breaches and what structural fix removes each. This guide answers what to check first.

The web and API vulnerability classes that cause most real breaches, each with the one structural fix that removes it.

appsecowaspweb-securityssrfxssaccess-controlmisconfiguration
Overview

The OWASP Top 10 ranks vulnerability classes by how often and how badly they appear in real applications. Read it as: each class has a structural fix (a framework default, a central function, a config) that removes the whole category instead of patching instances. Use it as a design and review checklist, not a compliance target; payment, defence and telecom teams already do.

Key facts

Use when

  • Designing a web app or API, writing a review checklist, scoping a pentest, in any field: payments (PCI DSS references it), defence and government (DISA STIG, US FTC), telecom (injection fines).

Avoid when

  • Treating it as complete: business-logic abuse and rate limits need their own threat modeling.

Deep dive

Decision table

RiskStructural fixDetail
Broken access control (incl. IDOR, SSRF)One allowed() check per object; deny by default; SSRF guard on outbound fetchesAuthorization models
Security misconfigurationHardened config as code, no default creds, debug off, security headers, minimal surfaceLeast privilege
Software supply chain failuresLockfiles, scanning, pinned CI actions, signed artifactsSupply chain
Cryptographic failuresTLS everywhere, AEAD at rest, vetted libraries, no custom cryptoTLS 1.3, AEAD
Injection (SQL, OS command, XSS, template)Parameterized queries, no shell, auto-escaping templates, CSPInjection
Insecure designThreat model per feature; rate limits, quotas, abuse cases in specsMissing controls can't be patched later
Authentication failuresPasskeys/MFA, Argon2id, rate limiting, session rotationPasskeys, Argon2id
Software/data integrity failuresNo unsafe deserialization of untrusted data; signed updatesNever pickle/Java serialization on input
Logging and alerting failuresLog auth events, denials, admin actions; alert on anomalies; no secrets in logsBreaches take months to detect without it
Mishandling exceptional conditionsFail closed; uniform error responses; no stack traces to clientsErrors must not skip checks

Code

Security headers for HTML responses (nginx; equivalent middleware exists in every framework):

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;

SSRF guard in Rust for "fetch this URL" features (webhooks, previews, imports). Resolve once, check every address, then connect to the checked address so DNS rebinding can't swap it (reqwest = "0.12", url = "2", tokio).

use std::net::{IpAddr, SocketAddr};

fn is_public(ip: IpAddr) -> bool {
    match ip {
        IpAddr::V4(v4) => {
            let o = v4.octets();
            !(v4.is_private() || v4.is_loopback() || v4.is_link_local()   // 169.254/16 = cloud metadata
                || v4.is_unspecified() || v4.is_broadcast() || v4.is_documentation()
                || o[0] == 100 && (o[1] & 0xC0) == 64                      // 100.64/10 CGNAT
                || o[0] == 0 || o[0] >= 224)                               // this-net, multicast, reserved
        }
        IpAddr::V6(v6) => {
            if let Some(v4) = v6.to_ipv4_mapped() { return is_public(IpAddr::V4(v4)); }
            let s = v6.segments();
            !(v6.is_loopback() || v6.is_unspecified() || v6.is_multicast()
                || (s[0] & 0xfe00) == 0xfc00                               // unique local fc00::/7
                || (s[0] & 0xffc0) == 0xfe80)                              // link local fe80::/10
        }
    }
}

pub async fn safe_get(raw: &str) -> Result<reqwest::Response, String> {
    let url = url::Url::parse(raw).map_err(|e| e.to_string())?;
    if url.scheme() != "https" { return Err("https only".into()); }
    let host = url.host_str().ok_or("no host")?.to_owned();
    let port = url.port_or_known_default().unwrap_or(443);
    let addrs: Vec<SocketAddr> = tokio::net::lookup_host((host.as_str(), port))
        .await.map_err(|e| e.to_string())?.collect();
    let addr = *addrs.first().ok_or("no address")?;
    if !addrs.iter().all(|a| is_public(a.ip())) { return Err("non-public address".into()); }
    let client = reqwest::Client::builder()
        .resolve(&host, addr)                          // pin the checked IP
        .redirect(reqwest::redirect::Policy::none())   // redirects would bypass the check
        .timeout(std::time::Duration::from_secs(10))
        .build().map_err(|e| e.to_string())?;
    client.get(url).send().await.map_err(|e| e.to_string())
}

Better still: run fetchers in an egress-restricted network (no route to internal ranges or metadata), and enforce IMDSv2 on AWS.

Efficiency

Cost: most controls are one-time structural changes with negligible runtime cost; the real spend is review time and scan minutes.

Levers:

  • Central checks — one allowed() function and one SSRF-guarded fetch helper remove a whole class for one lookup per request or per fetch (a DNS resolution, pinned to the checked address) — all apps.
  • Security headers — the five headers above add order of 0.5 KB per HTML response; HTTP/2 header compression (HPACK) removes most repeat cost on a reused connection — web responses.
  • Diff-aware scanning — SAST on changed code per pull request (Semgrep supports baseline-commit scans), full scans nightly, DAST against staging on a schedule — CI budget.
  • Fix the pattern — a Semgrep rule banning raw query strings closes every instance at once — recurring finding classes.

Naive → standard → state of the art:

  • Naive: patch findings one by one after a yearly audit · high review cost, repeat findings
  • Standard: ASVS level 1 plus dependency and secret scanning · low cost, covers most apps
  • State of the art: central checks, pattern rules, diff-aware SAST, scheduled DAST; ASVS level 2 and a yearly pentest for sensitive data · order of minutes per pull request

How to use it

  1. Per feature: a short threat model (what's valuable, who attacks, which classes above apply).
  2. Pick an ASVS level (1 for most apps, 2 for apps with sensitive data) and turn its requirements into tests.
  3. CI: dependency scanning, SAST (Semgrep, CodeQL) on changed code, secret scanning; DAST against staging for headers and common injections.
  4. Before launch and yearly: an external pentest focused on access control and business logic, which scanners miss.

Pitfalls

  • Fixing one instance of a class (one SQL injection) instead of the pattern (all raw query strings).
  • Debug endpoints, admin panels, /actuator, .git/ or .env reachable in production.
  • Verbose errors with stack traces, SQL or internal hostnames.
  • Default credentials on databases, dashboards, queues.
  • Permissive CORS: Access-Control-Allow-Origin reflecting any origin with credentials.
  • File uploads served from your origin with user-controlled content type: stored XSS. Serve from a separate domain with Content-Disposition: attachment.
  • Rate limits missing on login, password reset, OTP and expensive endpoints.
  • Scanning in CI but never fixing the findings.

In production

  • Payments: PCI DSS references the Top 10.
  • Defence and government: the Defense Information Systems Agency (DISA STIG) and the US Federal Trade Commission reference it. Both claims: Wikipedia, OWASP.
  • Telecom: TalkTalk was fined £400,000 by the UK ICO after a 2015 SQL injection breach, an Injection-class failure (Wikipedia).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
SemgrepOCaml / Pythonsemgrep/semgrepbrew install semgrep / pip install semgrep————SAST with custom pattern rules; diff-aware CI scans
CodeQLQL (CLI in C++/Java)github/codeqlbrew install --cask codeql————query-based SAST; GitHub code scanning
OWASP ZAPJavazaproxy/zaproxybrew install --cask zap————DAST proxy and scanner
OSV-ScannerGogoogle/osv-scannerbrew install osv-scanner————lockfile vulnerability scan against OSV
gitleaksGogitleaks/gitleaksbrew install gitleaks————secret scanning for repos and history

Sources

  • OWASP Foundation, OWASP Top 10, 2021 and 2025 editions. site
  • OWASP Foundation, OWASP API Security Top 10, 2023. site
  • OWASP Foundation, Application Security Verification Standard (ASVS). site
  • OWASP Foundation, Server-Side Request Forgery Prevention Cheat Sheet. doc
  • MITRE, CWE Top 25 Most Dangerous Software Weaknesses. list

Used by

Systems (3):

Alternatives and related