Deep dive
Decision table
| Risk | Structural fix | Detail |
|---|---|---|
| Broken access control (incl. IDOR, SSRF) | One allowed() check per object; deny by default; SSRF guard on outbound fetches | Authorization models |
| Security misconfiguration | Hardened config as code, no default creds, debug off, security headers, minimal surface | Least privilege |
| Software supply chain failures | Lockfiles, scanning, pinned CI actions, signed artifacts | Supply chain |
| Cryptographic failures | TLS everywhere, AEAD at rest, vetted libraries, no custom crypto | TLS 1.3, AEAD |
| Injection (SQL, OS command, XSS, template) | Parameterized queries, no shell, auto-escaping templates, CSP | Injection |
| Insecure design | Threat model per feature; rate limits, quotas, abuse cases in specs | Missing controls can't be patched later |
| Authentication failures | Passkeys/MFA, Argon2id, rate limiting, session rotation | Passkeys, Argon2id |
| Software/data integrity failures | No unsafe deserialization of untrusted data; signed updates | Never pickle/Java serialization on input |
| Logging and alerting failures | Log auth events, denials, admin actions; alert on anomalies; no secrets in logs | Breaches take months to detect without it |
| Mishandling exceptional conditions | Fail closed; uniform error responses; no stack traces to clients | Errors 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
- Per feature: a short threat model (what's valuable, who attacks, which classes above apply).
- Pick an ASVS level (1 for most apps, 2 for apps with sensitive data) and turn its requirements into tests.
- CI: dependency scanning, SAST (Semgrep, CodeQL) on changed code, secret scanning; DAST against staging for headers and common injections.
- 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.envreachable in production. - Verbose errors with stack traces, SQL or internal hostnames.
- Default credentials on databases, dashboards, queues.
- Permissive CORS:
Access-Control-Allow-Originreflecting 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
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Semgrep | OCaml / Python | semgrep/semgrep | brew install semgrep / pip install semgrep | — | — | — | — | SAST with custom pattern rules; diff-aware CI scans |
| CodeQL | QL (CLI in C++/Java) | github/codeql | brew install --cask codeql | — | — | — | — | query-based SAST; GitHub code scanning |
| OWASP ZAP | Java | zaproxy/zaproxy | brew install --cask zap | — | — | — | — | DAST proxy and scanner |
| OSV-Scanner | Go | google/osv-scanner | brew install osv-scanner | — | — | — | — | lockfile vulnerability scan against OSV |
| gitleaks | Go | gitleaks/gitleaks | brew 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