Application & Web Security
Most breaches do not come from someone cracking encryption with a supercomputer. They come from ordinary application code that trusts data it should not trust. Secure coding (the discipline of writing software that resists attack) starts from one mindset shift: treat all external input as hostile until you have proven it is safe. "External" is broader than you think. It includes request parameters, HTTP headers, cookies, uploaded files, responses from third-party APIs, and even database rows that another tenant wrote. This is the section a working web developer needs most, because this is where the real-world damage happens.
The cost is not theoretical. IBM's Cost of a Data Breach 2025 report puts the global average breach at USD 4.44 million. (Notably, that is the first decline in five years, down about 9% from USD 4.88M, credited to faster, AI-assisted incident containment.) The average organisation still took around 241 days to find and contain a breach, and web-application attacks remain one of the top breach channels in Verizon's annual DBIR report.
5.1 The OWASP Top 10 — the industry checklist
The OWASP Top 10 (OWASP = Open Worldwide Application Security Project, a non-profit) is the canonical, community-built list of the most critical web-app security risks. It is assembled from real-world data, not opinion. For years the 2021 edition was the standard. In late 2025, OWASP released the 2025 edition (the 8th) — now the current authoritative list. It analysed roughly 175,000 CVEs (publicly catalogued vulnerabilities) mapped across 643 weakness types, drawn from about 2.8 million applications and 13 contributing organisations.
| # | OWASP Top 10:2025 (current) | What changed vs 2021 |
|---|---|---|
| A01 | Broken Access Control | Still #1; now absorbs SSRF |
| A02 | Security Misconfiguration | Jumped #5 → #2 (CI/CD ships faster than scanning) |
| A03 | Software Supply Chain Failures | Renamed/widened from "Vulnerable & Outdated Components" |
| A04 | Cryptographic Failures | — |
| A05 | Injection | SQLi, command injection, and XSS all live here |
| A06 | Insecure Design | — |
| A07 | Authentication Failures | — |
| A08 | Software or Data Integrity Failures | — |
| A09 | Security Logging & Alerting Failures | Renamed from "…Monitoring Failures" |
| A10 | Mishandling of Exceptional Conditions | NEW — bad error handling, fail-open logic, leaked stack traces |
The headline changes: SSRF was folded into A01, and two new themes (supply-chain integrity and exception handling) joined. Always trust owasp.org over secondary blogs for exact wording.
5.2 Broken Access Control & IDOR (the #1 risk)
The app correctly authenticates you (proves who you are) but fails to authorize (check whether you are allowed to touch this specific record). The classic form is IDOR (Insecure Direct Object Reference): the URL reads /orders/1043, you change it to /orders/1044, and you see someone else's order — because the server fetched the row by ID without ever checking ownership.
SELECT * FROM orders WHERE id = ? AND owner_id = current_user. Using unguessable IDs like UUIDs makes enumeration (guessing the next ID) harder — but a UUID is defence-in-depth, never a substitute for the ownership check. This is exactly the multi-tenant rule in this codebase: never trust a tenant_id sent in a query parameter; derive the tenant from the host or session, server-side.is_admin=true to a form post and your model blindly saves it.5.3 Injection — input treated as code, not data
Injection happens when attacker input is interpreted as code instead of data. SQL injection (SQLi) is the classic.
// VULNERABLE — string concatenation: input becomes part of the SQL
query("SELECT * FROM users WHERE email = '" + email + "'");
// attacker types: ' OR '1'='1' -- -> returns every row / login bypass
// FIXED — parameterized query (prepared statement)
// the ? is always data, never code
query(“SELECT * FROM users WHERE email = ?”, [email]);
Parameterization is the real fix: the database compiles the query structure first, then binds your input purely as a value, so input can never change the query's meaning. Frameworks' query builders/ORMs (Laravel Eloquent, Django ORM) parameterize by default — but raw queries (DB::raw, .raw()) reopen the hole the moment you concatenate a string into them. An ORM does not protect you if you hand-build raw SQL.
Command injection (CWE-78) is the OS-shell cousin: code builds a shell command from input, e.g. exec("ping " + host), and the attacker sets host = "8.8.8.8; rm -rf /". The fix order from CISA's "secure by design" guidance: (a) don't shell out — use a native library API; (b) if you must, use an API that passes arguments as an array, not a single shell string, e.g. execFile("ping", ["-c","4", host]); (c) allow-list validate with a strict regex like ^[a-z0-9]{3,10}$.
5.4 XSS — running your JavaScript in someone else's browser
XSS (Cross-Site Scripting) injects JavaScript that runs in another user's browser, where it can steal their session, log keystrokes, or read page data. Three types:
- Stored (worst)
- The payload is saved in the database — e.g. a product review containing
<script>— and served to every visitor. - Reflected
- The payload rides in a URL/parameter and is bounced straight back in the response. Delivered via a malicious link.
- DOM-based
- Purely client-side: JS writes untrusted data into the page via
innerHTML/document.write. The server never sees the payload.
The fix is context-aware output encoding: encode data for the exact place it lands. HTML body, HTML attribute, JavaScript string, URL, and CSS each need different encoding — one encoder does not fit all. Vue and React auto-escape text in templates ({{ }}). The danger is opting out with v-html / dangerouslySetInnerHTML; if you truly must render user HTML, sanitize it with DOMPurify first. For DOM XSS, avoid innerHTML and use textContent or safe DOM APIs. A modern Chromium layer, Trusted Types, makes unsafe DOM sinks throw an error unless data passes through a vetted policy. This is precisely why this codebase forbids raw v-html.
5.5 CSRF — riding your logged-in session
CSRF (Cross-Site Request Forgery) tricks your logged-in browser into firing a state-changing request (change email, transfer money) to a site where you are authenticated — your cookies ride along automatically. Defences, layered:
- SameSite cookie attribute —
Lax(the modern browser default) blocks cookies on cross-site POSTs while allowing top-level GET navigation;Strictis tighter but logs users out when they arrive via an inbound link;NonerequiresSecure. - Anti-CSRF token — a cryptographically random, unpredictable value tied to the session, sent in a hidden field/header and verified server-side (the "synchronizer token" pattern). Frameworks ship it: Laravel
@csrf, Djangocsrf_token.
SameSite alone is defence-in-depth, not a complete fix — mutating GETs and sibling-domain trust still need tokens. Make every state change a POST/PUT/DELETE, never a GET.
5.6 SSRF — making the server fetch the wrong URL
SSRF (Server-Side Request Forgery), folded into A01 in 2025, happens when your app fetches a URL the user supplies (image-from-URL, webhook, PDF renderer) and the attacker points it at internal infrastructure the server can reach but they cannot.
Attacker --> "fetch http://169.254.169.254/..." --> YOUR SERVER
|
(server can reach internal IPs) v
cloud metadata endpoint -> temp credentials
169.254.169.254, which returned temporary IAM credentials. The attacker used them to read S3 and steal data on 106 million customers (~30GB). Two root causes: the SSRF plus an over-privileged IAM role. With least privilege, the blast radius would have been tiny.Fixes: allow-list outbound destinations (scheme/host/port); block private and link-local ranges (169.254.x, 10.x, 127.x, the metadata IP); don't blindly follow redirects; require IMDSv2 (token-based metadata) on AWS; and network-segment the fetcher.
5.7 Misconfiguration, supply chain & secrets
Security Misconfiguration (#2 in 2025) means default passwords, debug mode left on in production, verbose stack traces, open S3 buckets, and missing security headers. It jumped to #2 because continuous deployment outpaced continuous scanning. Insecure deserialization (under A08) is turning attacker-supplied serialized objects back into live objects, which can trigger remote code execution via "gadget chains" — never deserialize untrusted data with native deserializers (Java, PHP, Python pickle); prefer plain JSON with a strict schema, and sign any blob you must accept.
Software Supply Chain Failures (A03) means you inherit every bug in your dependencies. Log4Shell (CVE-2021-44228) is the icon: a JNDI lookup in the Log4j logging library let attackers run arbitrary code just by getting a malicious string logged. Around 7,000 packages depended on it, so deep transitive chains stayed exploitable long after a patch existed. Defend with an SBOM (Software Bill of Materials), pinned and verified dependencies, scanners (Dependabot, Snyk, npm audit, composer audit), fast patching, and build-pipeline integrity checks.
.gitignore plus pre-commit scanners (gitleaks, truffleHog), and rotate immediately on exposure — a leaked key is compromised forever, so revoke it; deleting the commit does nothing.5.8 Secure coding fundamentals & security headers
The through-line behind every fix above:
| Principle | What it means |
|---|---|
| Input validation = allow-list | Define what's permitted, reject everything else. Deny-lists are always bypassable. Check type, length, format, range at the boundary. |
| Output encoding | Encode at the point of output, for the specific context (HTML/attr/JS/URL). |
| Parameterization | For any interpreter — SQL, OS shell, LDAP, XPath. |
| Least privilege | Minimal rights for every DB user, IAM role, service account, so a breach is contained. |
| Fail closed | Deny on error; never fail open, never leak stack traces. (The lesson behind 2025's new A10.) |
Security headers are cheap, high-leverage HTTP response headers that add defence-in-depth:
- Content-Security-Policy (CSP)
- Allow-lists where scripts/styles/images may load from — the strongest anti-XSS backstop. 2025 best practice: avoid
'unsafe-inline'; use per-request cryptographic nonces (or hashes) on inline scripts; and deploy withContent-Security-Policy-Report-Onlyfirst to watch violations before enforcing. - Strict-Transport-Security (HSTS)
- Forces HTTPS for a duration, blocking downgrade/MITM attacks, e.g.
max-age=31536000; includeSubDomains. - X-Frame-Options & CSP frame-ancestors
- Stop clickjacking (your page loaded in a hostile iframe). 2025 guidance: prefer the more granular
frame-ancestorsdirective. - X-Content-Type-Options: nosniff
- Stops MIME-sniffing — a browser executing an uploaded "image" as a script.
- Referrer-Policy & Permissions-Policy
- Round out the set. Grade your headers with securityheaders.com or Mozilla Observatory.
report-uri, fix what breaks, then switch to enforce. Turning on a strict CSP on day one is the fastest way to break your own site.Common mistakes
- Trusting client-side checks — all authorization must be server-side.
- Blacklist filtering instead of allow-listing.
- "The ORM protects me" while writing raw SQL.
- Escaping shell input instead of avoiding the shell entirely.
- Hiding a UI button but leaving the endpoint open (IDOR).
- Committing a
.env, then "deleting" it without rotating the secret. - Deploying CSP in enforce mode on day one and breaking the site.
- Leaking stack traces or raw status codes to end users.
Best practices
- Validate input with allow-lists at every boundary.
- Parameterize every query to every interpreter.
- Encode output for its exact context; sanitize with DOMPurify if you must render user HTML.
- Scope every data fetch to the current user/tenant on the server; deny by default.
- Use SameSite cookies plus anti-CSRF tokens for state changes.
- Grant least privilege to every role and account.
- Keep secrets out of code; scan dependencies and rotate leaked keys.
- Add the security-header set as defence-in-depth.