43 min read

> — A security community proverb (origin unknown; applies just as well to the web)

Prerequisites

  • 12

Learning Objectives

  • Explain why a small set of web vulnerability classes has persisted for over two decades and what structural fact keeps them alive.
  • Trace SQL and command injection to their root cause and prevent them with parameterized queries and safe APIs.
  • Distinguish stored, reflected, and DOM-based cross-site scripting and stop each with output encoding and a Content Security Policy.
  • Explain and prevent cross-site request forgery and server-side request forgery with anti-CSRF tokens, SameSite cookies, and egress controls.
  • Identify session-management and authentication flaws (session fixation, weak cookies) and harden against them.
  • Deploy a web application firewall as one layer of defense in depth and detect exploitation attempts in web server and WAF logs.

Chapter 13: Web Application Security: SQL Injection, XSS, CSRF, and the Attacks That Never Get Old

"The S in IoT stands for security." — A security community proverb (origin unknown; applies just as well to the web)

Overview

Sam Whitfield, Meridian's security engineer, keeps a screenshot on the wall of his cube. It is the OWASP Top 10 from 2007. Next to it, the OWASP Top 10 from 2021. Injection is on both. Cross-site scripting is on both (folded into a broader category in the newer one, but unmistakably there). Broken access control and broken authentication are on both. Fourteen years apart, written by different people, surveying a completely transformed internet — and the same handful of vulnerability classes own the list. The frameworks changed. The languages changed. The cloud arrived. And a teller-facing web form at a regional bank can still be broken by a technique a teenager used against a guestbook script in 2002.

This is the uncomfortable fact at the center of this chapter: web application vulnerabilities do not age out. They are not zero-days that get patched and disappear. They are categories of mistake — failures to separate data from code, failures to distinguish a legitimate request from a forged one, failures to remember that everything from the browser is attacker-controlled — and every new developer, framework, and feature is a fresh opportunity to make them again. The attacker does not need a new technique. They need you to forget an old one, once, on one endpoint, on a Friday afternoon.

In Chapter 12 you met the OWASP Top 10 as a map and learned the principles that prevent most of it: input validation, output encoding, secure-by-default design, and the software supply chain. This chapter goes operational on the web's most durable attacks. We will take each one — injection, cross-site scripting, request forgery, server-side request forgery, and the session and authentication flaws that tie them together — and for each we will do the same four things: show you what goes wrong in a real application, explain the mechanism precisely enough that you could spot it in a code review, show you the secure fix, and show you how to detect attempts in your logs. Because in the defender's seat, prevention and detection are not alternatives. You write the code correctly and you watch for the people trying to find the one place you didn't.

The running example is Meridian's online-banking portal — a web application that touches customer money, customer data, and a bank's regulatory survival. We will review it the way Sam and the SOC actually would: looking for the vulnerability, applying the fix, and standing up the controls (a web application firewall, security headers, structured logging) that catch what slips through.

In this chapter, you will learn to:

  • Explain why a stable set of web vulnerability classes endures, and use that insight to prioritize defenses.
  • Recognize SQL injection and command injection in source code, and eliminate them with parameterized queries and safe APIs — not with input filtering alone.
  • Tell stored, reflected, and DOM-based XSS apart, and stop each with context-aware output encoding and a Content Security Policy (CSP).
  • Prevent CSRF with anti-CSRF tokens and SameSite cookies, and prevent SSRF with allowlists and egress filtering.
  • Harden sessions and authentication against session fixation and cookie theft.
  • Deploy a web application firewall (WAF) as defense in depth and write detections that surface injection and XSS attempts in web and WAF logs.

Learning Paths

This chapter sits at the intersection of building and defending, so it serves three audiences with different emphases.

🏗️ Security Engineer: This is core territory. Read all of §13.2–§13.5 closely — these are the fixes you will demand in code review and bake into frameworks. §13.7's secure-defaults thinking is your deliverable. 🛡️ SOC Analyst: Weight §13.6 most heavily (detecting attacks in WAF and web logs) and skim the mechanism sections enough to recognize an attack string when it scrolls past in an alert. The log-analysis exercises are written for you. 📜 Certification Prep: Injection, XSS, CSRF, SSRF, and secure session management appear across CompTIA Security+ (Secure Coding, Application Attacks) and CISSP (Software Development Security). The key-takeaways.md attack→fix→detect table is your revision sheet. 📋 GRC: Skim §13.1 and §13.6; you need to know these risks exist, map to PCI-DSS requirements, and require a remediation and WAF control — the depth belongs to the engineers.


13.1 Why web attacks endure

Before any technique, sit with the question Sam's two screenshots pose: why is this list so stable? The answer is not that developers are careless. Most are conscientious people under deadline pressure. The answer is structural, and understanding it will make you a better defender than memorizing any single attack.

A web application is, at its core, a machine for taking untrusted input from strangers and doing something useful with it. That is not a side effect of web apps; it is their entire purpose. A bank's login form must accept whatever a customer types. A search box must accept arbitrary text. A profile page must accept a display name the user chose. Every one of those inputs crosses a boundary — from the attacker-controllable browser into your trusted server — and at every boundary, the same failure is possible: the input gets treated as code instead of data. A username becomes part of a database command. A display name becomes part of a web page. A URL parameter becomes part of a request your server makes. The web's durability problem is the confusion of data and code, repeated at boundary after boundary, in every application ever written.

This is why the fixes in this chapter rhyme. Parameterized queries stop SQL injection by telling the database "this is data, never code." Output encoding stops XSS by telling the browser "this is text to display, never markup to execute." Anti-CSRF tokens stop request forgery by proving a request came from your own page and not a stranger's. The unifying principle, which you will hear in every section, is a single sentence worth memorizing:

🚪 Threshold Concept: Almost every web vulnerability is a failure to keep data and code in separate lanes. The attacker's whole game is to get something they control (data) interpreted as something that executes (code) — in a database, a browser, a shell, or a server's outbound request. Every durable fix in this chapter works by re-establishing that boundary structurally, so the interpreter can never again mistake the attacker's data for your instructions. Once you see web security this way, the attacks stop being a list to memorize and become variations on one mistake.

Three forces keep the old attacks alive, and naming them shapes how you defend:

  • The boundaries multiply. Every new feature is a new input. A modern banking portal has hundreds of endpoints, each accepting parameters, each a potential injection point. You have to be right on all of them; the attacker needs one you missed (Theme 2 — the asymmetry from Chapter 1).
  • Defaults are dangerous, and safe patterns require knowing them. String-concatenating a SQL query is the obvious way to build one; parameterization is the way you have to be taught. A template that does not auto-escape is happy to render an attacker's markup. The insecure path is frequently the path of least resistance, which is why secure frameworks and defaults (Chapter 12's secure-by-default principle, and §13.7 here) matter more than developer willpower.
  • The blast radius keeps growing. When these bugs were found in 2002 guestbooks, the damage was a defaced page. Today the same bug class sits in front of customer funds, health records, and cloud-metadata services that hand out credentials. The technique aged perfectly; only the stakes changed.

🔗 Connection: In Chapter 12 you saw injection and broken access control sitting atop the OWASP Top 10, and you learned input validation and output encoding as principles. This chapter is the operational sequel: it takes the specific web attacks those principles defend against and shows the exact code-level fix and the exact log signature for each. Chapter 12 told you what the categories are; Chapter 13 makes you able to fix and detect them.

A defender's framing for the whole chapter: you will deploy controls at three layers, and the discipline is to use all of them rather than betting on one. In the code (parameterized queries, output encoding, CSRF tokens) — the only place vulnerabilities are truly fixed. In the browser (CSP, SameSite cookies, security headers) — defense in depth that limits damage when the code is wrong. At the edge (a WAF) — a coarse net that buys time and generates telemetry, but never a substitute for the first two. Keep that three-layer picture in mind; it organizes everything that follows.

🔄 Check Your Understanding: 1. In one sentence, state the single root cause that unifies SQL injection, XSS, and command injection. 2. Why does the offense/defense asymmetry from Chapter 1 make web application security especially hard?

Answers

  1. Attacker-controlled data is allowed to be interpreted as code by some interpreter (a database, a browser, a shell) — the failure to keep data and code in separate lanes. 2. A web app exposes many input boundaries (endpoints, parameters, fields); the defender must keep every one safe, while the attacker only needs to find the single boundary that was missed.

13.2 Injection and parameterization

We start with injection, because it has topped or near-topped every web vulnerability list for two decades and because it is the cleanest illustration of the data-versus-code idea. The OWASP Top 10 you met in Chapter 12 lists injection as a top category; here we go to the code.

What goes wrong

Meridian's online-banking portal has an internal admin tool that lets support staff look up a customer by their login name. An early version of the lookup was written like this:

# VULNERABLE — do not ship. Customer lookup by username.
import sqlite3

def find_customer(conn, username):
    # The username comes straight from an HTTP request parameter.
    query = "SELECT id, name, email FROM customers WHERE username = '" + username + "'"
    return conn.execute(query).fetchall()

For an ordinary username like jlopez, the database receives:

SELECT id, name, email FROM customers WHERE username = 'jlopez'

That works. The problem is that the username is concatenated into the query as raw text, so the database cannot tell where your instruction ends and the user's data begins. The user controls part of the command, not just part of the data. SQL injection is the vulnerability in which attacker-controlled input is interpreted as part of a SQL statement, letting an attacker alter the query's logic — reading, modifying, or destroying data the application never meant to expose. If a support tool like this is reachable by an attacker (through a compromised account, or because a similar pattern exists on a customer-facing endpoint), the consequences range from dumping the entire customer table to bypassing authentication to deleting records.

We will not write a working data-extraction payload here — that is the offensive volume's territory, and a defensive book does not hand out turnkey exploits. What you need as a defender is the shape of the problem: when input is concatenated into a query, a value containing SQL syntax (a quote, a comment marker, a boolean clause) changes the structure of the statement rather than just filling in a blank. That structural change is the entire vulnerability.

   Figure 13.1 — How injection happens (data crossing into the code lane)

   INTENDED:   SELECT ... WHERE username = '[ data ]'
                                            └── user's value stays inside the quotes

   INJECTED:   SELECT ... WHERE username = '[ data that contains SQL syntax ]'
                                            └── a quote ends the string early, and
                                                what follows is parsed as CODE
                          ┌─────────────── the data/code boundary has moved ──────────┐
   Trusted (your code):   SELECT id,name,email FROM customers WHERE username =
   Untrusted (the input): '..............................................'
                          ▲ the application LET the input redraw this line

The secure fix: parameterized queries

The fix is not to "filter out bad characters." Blocklisting quotes and keywords is brittle, breaks legitimate input (the customer named O'Brien), and is bypassable in ways a defender should assume will eventually be found. The correct, structural fix is a parameterized query (also called a prepared statement): a query in which the SQL command and the data are sent to the database separately, with placeholders marking where data goes, so the database treats the supplied values strictly as data and never parses them as part of the command. The boundary between code and data is enforced by the database driver, not by your string-handling.

# FIXED — parameterized query. The ? is a placeholder; `username` is bound as DATA.
import sqlite3

def find_customer(conn, username):
    query = "SELECT id, name, email FROM customers WHERE username = ?"
    return conn.execute(query, (username,)).fetchall()

Now the command structure is fixed before the user's value is ever involved. Whatever username contains — quotes, semicolons, the entire text of a SQL tutorial — the database uses it only to compare against the username column. It cannot become code, because the parser already finished parsing the command. This is the same idea as §13.1's boundary, made concrete: the placeholder is the boundary, enforced structurally.

⚠️ Common Pitfall: "We sanitize inputs, so we're safe from injection." Input sanitization is a useful defense-in-depth layer, but it is not the fix, and treating it as the fix is how injection survives. Escaping functions get applied inconsistently, miss an endpoint, or fail to account for a context (numeric fields, identifiers, LIKE clauses, second-order injection where data is stored then later concatenated). Parameterized queries remove the vulnerability by construction; reach for them first and always. Use an ORM or query builder's parameter binding for the same reason — but verify it actually parameterizes, because raw-SQL escape hatches in ORMs reintroduce the bug.

A note on the cases parameterization does not cover: you cannot parameterize a table or column name (identifiers are part of the command, not data). If an application must choose a column from user input — say, a sort field — you allowlist it against a fixed set of known-good names and reject anything else. Likewise, command injection (the same mistake against a shell instead of a database — concatenating user input into an OS command) is fixed the same structural way: do not build a shell string; pass arguments as a list to an API that executes the program directly without a shell, so the input can never be interpreted as shell syntax.

# Command injection — the same lesson in a different interpreter.
import subprocess

# VULNERABLE: user input concatenated into a shell command string.
#   subprocess.run("ping -c 1 " + host, shell=True)   # host could carry shell syntax

# FIXED: arguments passed as a list, no shell to interpret them.
def ping_host(host):
    # `host` is a single argument; it cannot become a new command.
    return subprocess.run(["ping", "-c", "1", host], capture_output=True)

How to detect injection attempts in logs

Prevention is the fix; detection tells you who is trying, which informs whether you are being actively targeted and whether your other defenses are being probed. Injection attempts have recognizable shapes in logs, because to test for injection an attacker must send values containing SQL or shell metacharacters into parameters that normally hold plain data. You watch for:

  • Query parameters or POST bodies containing SQL keywords and syntax in unusual places — UNION, SELECT, OR 1=1-style boolean tautologies, comment sequences (--, /*), stacked ;, and bursts of single quotes.
  • A spike of HTTP 500 errors from one source — early injection probing often breaks queries before it succeeds, and database errors surface as application errors.
  • The same parameter on the same endpoint receiving structurally weird values from one client across many requests (automated fuzzing).

A simple, defensive SIEM-style query (you will build real detections in Chapter 21) sketches the idea:

-- Illustrative detection: SQL-injection probing in web access logs.
-- (Schema is notional; adapt to your SIEM in Ch.21.)
SELECT src_ip, COUNT(*) AS hits
FROM web_access_logs
WHERE request_uri RLIKE '(?i)(union\\s+select|or\\s+1=1|--|/\\*|;\\s*drop)'
   OR status_code = 500
GROUP BY src_ip
HAVING hits > 20          -- many probes from one source in the window
ORDER BY hits DESC;

🛡️ Defender's Lens: Be honest about what this detects: unsophisticated or high-volume injection probing. A patient attacker can encode payloads, use blind techniques that produce no obvious keywords, and stay under thresholds. So this detection is a tripwire, not a guarantee — it catches the noisy majority and the scanners, and it tells you when someone is poking at a specific endpoint. The fix (parameterization) is what makes you safe; the detection is what makes you aware. Never let a WAF or a log rule become the reason a query stays vulnerable.

🔄 Check Your Understanding: 1. Why is a parameterized query a structural fix for SQL injection, while input filtering is only a partial defense? 2. You cannot parameterize a column name supplied by the user (e.g., a sort field). What do you do instead?

Answers

  1. Parameterization sends the command and the data to the database separately, so the command is fully parsed before the data is involved — the data can never alter the command's structure. Filtering tries to anticipate every dangerous character/context and inevitably misses one. 2. Allowlist the value against a fixed set of known-good column names and reject anything not on the list.

13.3 XSS and CSP

Injection puts the attacker's data into a database command. Cross-site scripting puts it into a web page. The interpreter changes from the SQL engine to the browser, but the root cause is identical: untrusted data is rendered into a context where it is interpreted as code — here, as HTML and JavaScript that runs in another user's browser, with that user's session and permissions.

Cross-site scripting (XSS) is the vulnerability in which an attacker injects script into web content that is then executed by another user's browser in the context of the trusted site. Because the script runs as the victim, with the victim's authenticated session, it can read the page, steal session tokens or anti-CSRF tokens, perform actions as the victim (transfer money, change an email address), or rewrite what the victim sees. For a bank, that is catastrophic. XSS comes in three forms, distinguished by where the malicious data lives and how it reaches the browser.

The three forms

Stored XSS (also called persistent XSS) is the most dangerous: the attacker's script is saved on the server — in a database, a profile field, a support-ticket comment, a transaction memo — and then served to every user who views that content. One injection, many victims, no further action required from the attacker. At a bank, imagine a "memo" field on a transfer that a back-office employee later views in an admin console; if that console renders the memo without encoding, the attacker's script runs in the employee's privileged browser.

Reflected XSS is non-persistent: the attacker's script is included in a request (typically a URL parameter), and the server reflects it straight back into the immediate response — a search page that echoes "No results for [your query]" without encoding, for instance. The script is not stored; it runs only for someone who follows the attacker's crafted link. Delivery therefore relies on social engineering (a phishing link), which connects directly to the Chapter 9 email threats — a reflected-XSS link in a convincing email is a potent combination.

DOM-based XSS never involves the server reflecting anything. The vulnerability is entirely in client-side JavaScript that reads attacker-controllable input (the URL fragment, location, document.referrer) and writes it unsafely into the page (via innerHTML, document.write, and similar "sinks"). Because the dangerous flow happens in the browser, server-side logs may show nothing unusual, which makes DOM XSS both easy to miss and harder to detect — a point that matters when we get to logging.

   Figure 13.2 — The three flavors of XSS (where the payload lives)

   STORED:     attacker → [ saved on server ] → served to MANY users → runs in each browser
                          (profile, comment, memo, ticket)

   REFLECTED:  attacker → crafts URL → victim clicks → server echoes input → runs once
                          (delivered via a phishing link; see Ch.9)

   DOM-BASED:  attacker → crafts URL → victim's page JS reads it → writes to DOM unsafely
                          (server never sees the payload; flow is entirely client-side)

The secure fix: context-aware output encoding (plus a safe-sink discipline)

The primary fix for stored and reflected XSS is output encoding (also called output escaping): converting data into a form that the browser will display as literal text rather than interpret as markup or script, applied at the point the data is written into the page and appropriate to the context (HTML body, HTML attribute, JavaScript, URL, or CSS each need different encoding). You met output encoding as a principle in Chapter 12; here is the operational rule: encode on output, in the right context, every time untrusted data meets the page.

# FIXED (reflected): HTML-encode untrusted data before placing it in the page body.
import html

def render_search_heading(query):
    # `query` came from the URL. Encode it so '<', '>', '&', quotes become entities.
    safe = html.escape(query)            # <script> becomes &lt;script&gt;
    return f"<h2>No results for {safe}</h2>"

# Expected behavior:
#   render_search_heading("loan rates")          -> "<h2>No results for loan rates</h2>"
#   render_search_heading("<script>x</script>")  -> "<h2>No results for &lt;script&gt;x&lt;/script&gt;</h2>"
#   In the second case the browser DISPLAYS the text; it does not run a script.

In practice you should rely on a template engine that auto-escapes by default (modern engines like Jinja2, Razor, React's JSX do this for the HTML-body context), so the safe behavior is the default and a developer has to go out of their way — using a raw/unescaped construct — to create the bug. That is the secure-by-default principle from Chapter 12 doing real work. For DOM-based XSS, the fix is a safe-sink discipline in client code: never pass untrusted data to dangerous sinks like innerHTML or document.write; use safe APIs (textContent, setAttribute with care) that cannot introduce markup, and treat the URL and fragment as untrusted input.

⚠️ Common Pitfall: Encoding for the wrong context. HTML-encoding a value is correct when it lands in the HTML body, but if untrusted data is written inside a <script> block, into an HTML attribute, or into a URL, HTML-entity encoding is insufficient or wrong, and the data can still break out. The rule is context-aware encoding: pick the escaping that matches exactly where the data is going. When you find yourself hand-building a <script> string with user data inside it, stop — that is almost always a design smell. Put data in a data- attribute or a JSON block that your script reads via a safe DOM API instead.

Defense in depth: Content Security Policy

Even with disciplined encoding, you assume (Theme 4) that one endpoint will eventually be wrong. The browser-side safety net is a Content Security Policy (CSP): an HTTP response header that tells the browser which sources of script, style, images, and other resources are allowed to load and execute for a page, so that even if an attacker injects a <script>, the browser refuses to run it because it violates the policy. CSP does not fix XSS — vulnerable code is still vulnerable — but a good CSP can turn a successful injection into a blocked, reported non-event. It is the seatbelt that matters precisely when the code (the brakes) has failed.

A pragmatic, strong CSP for a banking portal looks like this, delivered as a response header (these directives were introduced as part of the HTTP-security-header baseline in Chapter 9; here we make the policy concrete):

Content-Security-Policy:
  default-src 'self';
  script-src 'self';                 # only scripts served from our own origin run
  object-src 'none';                 # no Flash/plugins
  base-uri 'self';                   # attacker can't repoint relative URLs
  frame-ancestors 'self';            # clickjacking defense (replaces X-Frame-Options)
  report-uri /csp-violation-report   # browser POSTs violations here -> telemetry

The single most valuable line for XSS defense is script-src 'self' (with no 'unsafe-inline'): it tells the browser to run only scripts loaded from your own origin and to refuse inline scripts entirely. An injected <script>alert(1)</script> is inline, so the browser blocks it. The cost is real — you must move inline scripts and event handlers into served .js files (or use nonces/hashes for the few you cannot) — and that engineering effort is exactly why weak CSPs with 'unsafe-inline' are common and nearly worthless against XSS. A defender pushes for the strict policy and treats 'unsafe-inline' in a script-src as a finding.

🔗 Connection: Recall from Chapter 9 that the HTTP-security-header baseline (Content-Security-Policy, X-Content-Type-Options, X-Frame-Options/frame-ancestors, HSTS) was set as Meridian's web-hardening standard. Chapter 9 established that the headers exist and should be on; this chapter defines what a strong CSP actually says and why script-src 'self' is the line that defeats injected inline script. The same frame-ancestors 'self' directive doubles as the modern clickjacking defense.

Detecting XSS attempts

Detection differs sharply by XSS type, and a good defender knows why:

  • Reflected and stored XSS leave fingerprints in server-side logs and request bodies: values containing <script, onerror=, onload=, javascript:, <img, <svg, or HTML-entity/URL-encoded variants of these, appearing in parameters and form fields that should hold plain text. A surge of such values from one source is probing.
  • Stored XSS additionally warrants content monitoring — periodically scanning stored fields (profile names, memos, comments) for script-like content, because the injection request might be long past while the payload sits waiting in your database.
  • DOM-based XSS is the hard one: the payload often lives in the URL fragment (after the #), which browsers do not send to the server, so your access logs may be blind to it. This is the strongest argument for the CSP report-uri/report-to directive — when the browser blocks a violation, it POSTs a report to you, giving you telemetry on attacks your server-side logs cannot see.
-- Illustrative detection: reflected/stored XSS probing in web logs.
SELECT src_ip, request_uri, COUNT(*) AS hits
FROM web_access_logs
WHERE request_uri RLIKE '(?i)(<script|onerror=|onload=|javascript:|%3cscript|<svg|<img[^>]+src)'
GROUP BY src_ip, request_uri
HAVING hits > 5
ORDER BY hits DESC;
-- Pair this with CSP violation reports for the DOM-XSS coverage gap.

🔄 Check Your Understanding: 1. Why is stored XSS generally more dangerous than reflected XSS? 2. A team adds a Content Security Policy but keeps 'unsafe-inline' in script-src "because rewriting our inline scripts is too much work." Has CSP meaningfully improved their XSS posture? Why or why not? 3. Why can server-side access logs be blind to DOM-based XSS, and what control compensates?

Answers

  1. Stored XSS is saved on the server and served to every user who views the content, requiring no per-victim action; reflected XSS runs only for a victim who follows a crafted link, so it needs social engineering and hits one user at a time. 2. No, not meaningfully against XSS: 'unsafe-inline' permits exactly the injected inline <script> that XSS relies on, so the policy stops little. The strong line is script-src 'self' with no 'unsafe-inline'. 3. DOM-XSS payloads often live in the URL fragment (after #), which browsers do not transmit to the server, so it never appears in access logs; CSP violation reporting (report-uri/report-to) gives client-side telemetry to compensate.

13.4 CSRF and SSRF

The next two attacks both abuse trust, but in opposite directions. CSRF abuses the trust a server places in a browser's automatically-attached credentials. SSRF abuses the trust a server places in its own outbound requests. They are easy to confuse by name and important to keep distinct.

Cross-site request forgery (CSRF)

Cross-site request forgery (CSRF) is an attack in which a malicious page causes a victim's browser to send an unwanted, state-changing request to a site where the victim is already authenticated, exploiting the fact that browsers automatically attach the victim's session cookie to requests to that site. The victim need not interact with anything malicious beyond visiting the attacker's page (or an attacker's ad, or a forum post with an image tag); the browser does the rest, using credentials it attaches by default. The classic target is a state-changing action that relies only on the session cookie for authorization — a funds transfer, an email-address change, a password reset.

Here is the mechanism, because it is subtle and the fix follows directly from it. Meridian's portal exposes POST /transfer to move money. The browser, when it sends that request, automatically includes the customer's session cookie — that is how the bank knows who is asking. Now the customer, still logged in, visits an unrelated malicious site in another tab. That site contains a hidden form (or a scripted fetch) that submits a POST to https://onlinebanking.meridianbank.example/transfer with the attacker's account as the destination. The browser attaches the customer's Meridian session cookie because that is what browsers do for requests to a site, and the bank's server, seeing a valid session, processes the transfer. The request was authenticated but not intended. That gap — authenticated-but-not-intended — is the whole vulnerability.

   Figure 13.3 — CSRF: the browser's automatic cookie is the attack's fuel

   1. Victim logs into bank        → browser holds bank session cookie
   2. Victim (still logged in) visits attacker's page in another tab
   3. Attacker's page tells the browser:  POST /transfer  to=ATTACKER  amount=5000
   4. Browser ATTACHES the bank cookie automatically (same-site request rules permitting)
   5. Bank sees a valid session → executes the transfer

   Root cause: the server trusted the cookie ALONE to prove intent.
   Fix: demand a secret the attacker's page cannot know or read (a CSRF token),
        and tell the browser not to attach the cookie cross-site (SameSite).

The secure fix is twofold, and you use both:

  1. Anti-CSRF tokens (the synchronizer-token pattern): the server embeds a secret, unpredictable, per-session (or per-request) token in its own forms and requires it back on every state-changing request. The attacker's page can cause the browser to send a request, but it cannot read or guess the token (the same-origin policy, below, prevents the attacker's page from reading Meridian's pages), so the forged request lacks the token and is rejected. Most web frameworks provide this; the defender's job is to ensure it is enabled for all state-changing endpoints and not accidentally disabled.

  2. SameSite cookies: an attribute on the session cookie (SameSite=Lax or SameSite=Strict) that instructs the browser not to attach the cookie to cross-site requests — which removes the fuel the attack runs on. Lax (a sensible default, and the default in modern browsers) blocks the cookie on cross-site sub-requests like the forged POST while still allowing top-level navigations; Strict is tighter. Combined with the token, you have defense in depth: even if one mechanism is misconfigured on some endpoint, the other still bites.

The deeper reason the token works is the same-origin policy (SOP): a fundamental browser security rule that restricts how a document or script loaded from one origin (scheme + host + port) can interact with resources from another origin — in particular, it prevents a page from one origin from reading the responses or contents of a page from a different origin. SOP is why the attacker's site can send a request to Meridian but cannot read Meridian's pages to steal the CSRF token, and it is one of the load-bearing assumptions behind much of web security. (CSP, above, complements SOP; CORS is the controlled, deliberate relaxation of it — both topics you will see again in any deeper web-security study.)

⚠️ Common Pitfall: Assuming an API is CSRF-immune because it "uses JSON" or "isn't a form." Whether CSRF applies depends on how the request is authenticated, not its content type. If your endpoint authenticates via an automatically-attached cookie, it is a CSRF candidate regardless of body format. APIs authenticated by a bearer token in an Authorization header that the browser does not attach automatically are not classically vulnerable — but the moment you fall back to cookie auth for a browser app, CSRF is back on the table. Decide deliberately; do not assume.

Detecting CSRF is mostly about control verification rather than log-hunting, because a successful CSRF request looks, by design, like a legitimate authenticated request. The high-value signals are: state-changing requests arriving with a missing or invalid CSRF token (your application should log and reject these — a rejected-token spike is a strong indicator), and state-changing requests with a cross-site Origin/Referer header (checking these is a useful secondary defense). The most important "detection," though, is a test: confirm in code review and dynamic testing that every state-changing endpoint requires a valid token and that the session cookie is SameSite and Secure and HttpOnly.

Server-side request forgery (SSRF)

Server-side request forgery (SSRF) is an attack in which an attacker induces a server to make an outbound request to a destination of the attacker's choosing, abusing the server's network position and trust to reach systems the attacker could not reach directly — internal services, cloud metadata endpoints, or other back-end systems behind the firewall. SSRF rose to prominence precisely because cloud environments made it devastating: a server tricked into requesting its cloud provider's instance metadata endpoint can retrieve temporary credentials, turning a "fetch a URL" feature into cloud account compromise. (This is the boundary where this chapter touches Chapter 15's cloud material; here we focus on the web-app-level fix.)

The pattern is any feature where the application fetches a URL or address that the user influences: "import an image from a URL," "validate this webhook," "preview this link," "fetch this document." If the user controls the destination and the server does not constrain it, the attacker points the server's request inward — at 169.254.169.254 (the well-known link-local metadata address), at 127.0.0.1, at internal-only services — and the server, which can reach those, does so on the attacker's behalf.

The secure fix is layered, because SSRF is genuinely hard to fully prevent at the application layer alone:

  • Allowlist destinations. If the feature should only reach a known set of hosts (a specific partner API, an approved image CDN), enforce an allowlist of exact hosts/domains and reject everything else. Allowlisting beats blocklisting here for the same reason it does everywhere — you cannot enumerate every dangerous internal address (and DNS rebinding and redirects defeat naive checks).
  • Block access to internal ranges and the metadata IP. Resolve the destination and refuse private/loopback/link-local ranges (RFC1918 10/8, 172.16/12, 192.168/16; loopback 127.0.0.0/8; link-local 169.254.0.0/16). Crucially, re-check after DNS resolution and after every redirect, because an attacker can use a hostname that resolves to an internal IP or a redirect that lands inside.
  • Egress filtering at the network. Defense in depth from Chapter 6/7: the application server should not be able to reach the metadata endpoint or arbitrary internal hosts. Restrict its outbound network access so that even a successful application-layer SSRF hits a network wall. In cloud, use the hardened metadata service version that requires a session token, which raises the bar significantly.
# Sketch of the application-layer SSRF guard (illustrative; combine with network egress controls).
import ipaddress, socket

ALLOWED_HOSTS = {"images.partner.example", "api.partner.example"}

def is_safe_fetch_target(hostname):
    if hostname not in ALLOWED_HOSTS:        # 1) allowlist first
        return False
    ip = ipaddress.ip_address(socket.gethostbyname(hostname))  # 2) resolve, then check
    if ip.is_private or ip.is_loopback or ip.is_link_local:    # blocks 169.254.*, 127.*, 10.*, ...
        return False
    return True

# Expected output (hand-traced):
#   is_safe_fetch_target("images.partner.example") -> True   (on allowlist, public IP)
#   is_safe_fetch_target("169.254.169.254")        -> False  (not on allowlist)
#   is_safe_fetch_target("internal.meridian.local")-> False  (not on allowlist; also private)
# NOTE: real guards must also re-check after redirects and defend against DNS rebinding.

Detecting SSRF focuses on the server's outbound behavior, which is why network monitoring (Chapter 10) matters here: outbound requests from an application server to internal IP ranges, to 169.254.169.254, or to unexpected destinations are high-signal indicators. Application logs that record the resolved destination of user-influenced fetches, alerts when a fetch target resolves to a private/link-local address, and network egress logs showing the app server reaching places it never should — these are your detections. An app server that suddenly queries the cloud metadata endpoint is a five-alarm event.

🔄 Check Your Understanding: 1. In one sentence each, contrast what CSRF abuses versus what SSRF abuses. 2. Why does the same-origin policy make anti-CSRF tokens effective? 3. For an SSRF-prone "fetch this URL" feature, why is allowlisting destinations preferred over blocklisting internal addresses?

Answers

  1. CSRF abuses the trust a server places in the browser's automatically-attached session cookie (tricking the browser into sending an authenticated request the user did not intend); SSRF abuses the trust/network-position of the server itself (tricking the server into making an outbound request to an attacker-chosen, often internal, destination). 2. SOP prevents the attacker's page from reading the victim site's pages, so it cannot obtain the secret CSRF token it would need to forge a valid request. 3. You cannot reliably enumerate every dangerous internal address, and DNS rebinding/redirects defeat naive blocklists; an allowlist of known-good destinations fails closed and is far more robust.

13.5 Session and authentication flaws

Injection and XSS get the headlines, but a great many real web compromises come from mishandling the thing that ties a user's requests together: the session. After a user authenticates (Chapter 16 owns authentication in depth; here we cover the web-session mechanics that sit on top of it), the server issues a session identifier — usually a cookie — that the browser presents on every subsequent request to prove "I am still the person who logged in." If an attacker can predict, steal, fixate, or fail to have invalidated that identifier, they become the user without ever knowing the password.

The flaws and their fixes

Session fixation is an attack in which the attacker causes a victim to authenticate using a session identifier the attacker already knows (for example, by setting the session ID before the victim logs in), so that after login the attacker — holding the same ID — is now inside the victim's authenticated session. The mechanism depends on the application keeping the same session ID across the privilege change from anonymous to authenticated. The fix is direct and structural: regenerate the session identifier on every privilege change, especially at login (and discard the old one). If the ID the victim authenticates with is brand-new and never seen by the attacker, fixation collapses.

# Session fixation — the fix is to rotate the session ID at login.
# (Framework-agnostic sketch; your framework has a specific call for this.)

# VULNERABLE: the same session id that existed before login is reused after login.
#   session.user_id = user.id           # attacker who pre-set the session id is now "in"

# FIXED: issue a fresh session id at the moment of authentication.
def on_successful_login(session, user):
    session.regenerate_id()             # old id is invalidated; a NEW id is issued
    session.user_id = user.id           # the attacker's known id is now worthless

Beyond fixation, web-session hygiene is a short, high-leverage checklist a defender enforces:

  • Cookie flags. Set HttpOnly (so client-side JavaScript — including injected XSS — cannot read the session cookie), Secure (so it is only sent over HTTPS, complementing HSTS from Chapter 9), and SameSite (the CSRF defense from §13.4). These three flags are nearly free and shut down whole categories of theft and misuse. HttpOnly in particular is why XSS and session theft are partly decoupled — it does not fix XSS, but it denies the most common XSS payload (cookie theft) its prize.
  • Unpredictable, long session IDs. The identifier must be generated by a cryptographically secure random source with enough entropy that guessing is infeasible (this is the entropy idea from the cryptography chapters applied to tokens). A predictable or sequential session ID is a vulnerability on its own.
  • Expiration and invalidation. Sessions must time out (idle and absolute), and logout must actually invalidate the session server-side — not merely delete the cookie client-side, which leaves a still-valid token that a thief can replay. Re-authenticate for sensitive actions (a "step-up" before a wire transfer).
  • Bind and monitor. Watch for a single session ID used from two distant IPs or wildly different user-agents at once — a classic sign of a stolen/replayed session.

⚠️ Common Pitfall: "Logout just deletes the cookie." If logout only clears the cookie in the browser but the server still accepts that session ID, an attacker who captured the token before logout can keep using it. Invalidation must happen server-side. The same logic applies to password changes and detected compromise: the correct response is to invalidate all of that user's active sessions, not to trust the client to forget.

How these connect to everything else in the chapter

Notice the web: XSS (§13.3) is the most common delivery for session theft, which is why HttpOnly matters; CSRF (§13.4) is the abuse of the session's automatic presentation, which is why SameSite matters; and weak session management makes both worse. These are not five separate topics but one system — authentication establishes identity, the session carries it, and every attack in this chapter is, in some sense, an attempt to ride that session. Defending the session well is therefore one of the highest-leverage things a web defender does.

🔗 Connection: Authentication factors — passwords, MFA, passkeys — and credential attacks like stuffing and spraying are Chapter 16's domain. This chapter assumes the user has authenticated and focuses on the web session that represents that authentication. The handoff is clean: Chapter 16 proves who you are; Chapter 13 protects the cookie that remembers it. A bank needs both, because phishing-resistant MFA (Chapter 1's hero control) is undermined if the post-login session can be stolen via XSS or fixed via session fixation.

🔄 Check Your Understanding: 1. What single change defeats session fixation, and why? 2. The HttpOnly cookie flag does not fix XSS — so why is it one of the most valuable mitigations against the consequences of XSS?

Answers

  1. Regenerating (rotating) the session ID at login: the attacker's pre-set/known ID is discarded, so the victim authenticates under a fresh ID the attacker never saw. 2. The most common XSS payload steals the session cookie; HttpOnly makes the session cookie unreadable to JavaScript, so even successful XSS cannot exfiltrate it — decoupling "script ran" from "session stolen."

13.6 WAFs and detecting attacks in logs

You have the code-level fixes (parameterization, encoding, tokens, session hygiene) and the browser-level layer (CSP, SameSite, security headers). The third layer lives at the edge, and it is where the SOC spends its time: the web application firewall and the logs it and your servers produce.

The WAF: a layer, not a cure

A web application firewall (WAF) is a security control that inspects HTTP/HTTPS traffic to and from a web application and applies rules to detect and block common web attacks (injection, XSS, and many others) before they reach the application, operating at the application layer rather than the network layer. (In Chapter 7 a WAF was named as a feature of next-generation firewalls; this chapter owns it as a web-application control.) A WAF sits in front of the app — as a reverse proxy, a cloud service, or a module — and matches requests against signatures (often built on a managed rule set such as the OWASP Core Rule Set) and increasingly against anomaly models. When a request matches an attack pattern, the WAF can block it, challenge it, or log it.

The defender's stance on WAFs must be precise, because they are routinely misunderstood in both directions:

  • A WAF is real defense in depth. It blocks the noisy majority of opportunistic and automated attacks, buys time to patch a newly discovered vulnerability (a virtual patch — a WAF rule that blocks exploitation of a specific bug while developers fix the code), and is a rich source of attack telemetry. For PCI-DSS-regulated applications like Meridian's, a WAF (or equivalent) is effectively expected.
  • A WAF is not a fix. It is a pattern-matcher in front of a vulnerability that still exists. Skilled attackers bypass WAFs with encoding, obfuscation, and novel payloads; over-tuned WAFs generate false positives that break legitimate traffic and pressure teams to weaken rules. The vulnerability is fixed in the code; the WAF buys time and visibility. Treat any argument of the form "we don't need to fix the SQL injection, the WAF blocks it" as the dangerous fallacy it is.

🛡️ Defender's Lens: The highest-value way to run a WAF in a mature shop is in blocking mode for high-confidence rules and detection (alert-only) mode for the rest, feeding everything to the SIEM (Chapter 21). The WAF then does double duty: it stops the easy attacks and it tells your SOC which endpoints are being probed and how — intelligence that drives where you point your next code review and penetration test. A WAF that only blocks, and is never read, throws away half its value.

Detecting attacks in logs

This is the SOC's core skill for web attacks, and it ties together the per-attack detection notes from the sections above into one practice. Three log sources matter, and they complement each other:

  1. Web server access logs (the request line, status, source, user-agent, referrer). These reveal injection and reflected/stored-XSS probing in the URI and query string, scanner user-agents, and error spikes.
  2. WAF logs (which rule fired, the matched pattern, the action taken). These are pre-correlated attack indicators — a gift to the analyst — but require tuning judgment to separate true probing from false positives.
  3. Application logs (rejected CSRF tokens, failed authorization, the resolved destinations of outbound fetches, session anomalies). These catch what the first two cannot — CSRF, SSRF, and session abuse that look like valid HTTP.

The patterns to hunt, consolidated:

Attack Where it shows Indicators to alert on
SQL/command injection Access logs, WAF, app errors SQL/shell metacharacters in params (union select, or 1=1, --, ;, backticks); HTTP 500 spikes from one source
Reflected/stored XSS Access logs, WAF, stored content <script, onerror=, javascript:, <svg, <img src (and encoded variants) in inputs; script-like content in stored fields
DOM-based XSS CSP violation reports (often not in access logs) Browser-reported violations to report-uri/report-to; payload often in the unlogged URL fragment
CSRF Application logs Spikes of rejected/missing CSRF tokens; state-changing requests with cross-site Origin/Referer
SSRF App logs + network egress (Ch.10) User-influenced fetches resolving to private/loopback/link-local IPs; app server reaching 169.254.169.254 or internal hosts
Session abuse Application logs One session ID from two distant IPs/user-agents; session use after logout; bursts of failed/rotated tokens

A consolidated, illustrative detection that an analyst might keep as a dashboard tile:

-- Illustrative: web attack probing rollup by source (adapt in Ch.21's SIEM).
SELECT src_ip,
       SUM(request_uri RLIKE '(?i)(union\\s+select|or\\s+1=1|--|/\\*)')             AS sqli_hits,
       SUM(request_uri RLIKE '(?i)(<script|onerror=|javascript:|<svg|%3cscript)')   AS xss_hits,
       SUM(CASE WHEN status_code = 500 THEN 1 ELSE 0 END)                            AS errors
FROM web_access_logs
WHERE event_time > NOW() - INTERVAL '15' MINUTE
GROUP BY src_ip
HAVING sqli_hits > 0 OR xss_hits > 0 OR errors > 25
ORDER BY (sqli_hits + xss_hits + errors) DESC;

📟 War Story: A constructed but representative incident. A SaaS company's WAF was in alert-only mode and nobody read its logs. For three weeks it had been firing dozens of SQL-injection rule matches an hour against a single reporting endpoint — an endpoint that, it turned out, built a query by string concatenation behind the WAF. The attacker was methodically working around the WAF's signatures; the WAF was telling the defenders this in real time, and the telemetry sat unread. The breach that followed was preventable twice over: by parameterizing the query, and by reading the alerts the company had already paid to generate. The lesson is the chapter in miniature — fix the code, and watch the logs, and never assume a control you don't monitor is protecting you.

🔄 Check Your Understanding: 1. Give two legitimate things a WAF does well and one thing it must never be used to justify. 2. Which web attack from this chapter is most likely to be invisible in web server access logs, and what control gives you telemetry on it?

Answers

  1. Well: blocks the noisy majority of automated/opportunistic attacks; provides virtual patching to buy time; generates attack telemetry for the SOC. Must never justify: leaving the underlying vulnerability unfixed in the code. 2. DOM-based XSS — its payload commonly lives in the URL fragment, which browsers do not send to the server; CSP violation reporting (report-uri/report-to) provides client-side telemetry.

Project Checkpoint

Meridian's program reaches its web-application security controls, the second half of the "Secure-SDLC + web-app controls" increment (the secure-SDLC policy was Chapter 12's contribution). And bluekit gains an illustrative taint-tracking demonstrator that makes the data-versus-code idea concrete in code.

Program increment — web-application security controls. Sam and the engineering team add a web-app security standard to Meridian's program with five enforceable requirements, each tracing to a section of this chapter and to PCI-DSS expectations for the cardholder-data-adjacent portal:

  1. Parameterized queries everywhere. No string-concatenated SQL; ORM parameter binding verified; identifiers allowlisted. (§13.2)
  2. Output encoding by default. Auto-escaping templates mandatory; raw/unescaped rendering requires security review. (§13.3)
  3. A strict CSP (script-src 'self', no 'unsafe-inline', frame-ancestors 'self', report-uri to the SIEM) on every customer-facing page. (§13.3)
  4. CSRF tokens + SameSite/Secure/HttpOnly cookies on all state-changing endpoints; session ID rotated at login; server-side logout. (§13.4, §13.5)
  5. A WAF in front of the portal, high-confidence rules blocking and the rest alerting to the SIEM, plus the §13.6 detections as standing dashboards. SSRF guards (allowlist + egress filtering) on every URL-fetch feature. (§13.4, §13.6)

This increment plugs directly into the secure pipeline you will build in Chapter 31 (where these checks become automated gates) and feeds the SIEM use cases of Chapter 21.

bluekit increment — appsec.py gains taint_demo(src, sink). Chapter 12 created appsec.py with scan_dependencies. We extend it with an illustrative taint-tracking demonstrator — the concept behind how static analysis (the SAST you met in Chapter 12) reasons about injection: data from an untrusted source that reaches a dangerous sink without passing through a sanitizer is "tainted" and flags a potential vulnerability. This is a teaching model of the idea, not a real analyzer, and as always it is never executed during authoring.

# bluekit/appsec.py  — Chapter 13 increment (illustrative; concept demo only)
"""taint_demo: a teaching model of taint tracking for injection.

Untrusted DATA from a `source` that reaches a `sink` without a `sanitizer`
in between is 'tainted' -> a possible injection. This mirrors how SAST reasons;
it is NOT a real static analyzer. See Ch.12 for SAST/DAST in practice.
"""

UNTRUSTED_SOURCES = {"request.args", "request.form", "request.body", "url_param"}
DANGEROUS_SINKS   = {"db.execute", "os.system", "subprocess(shell=True)", "innerHTML"}
SANITIZERS        = {"parameterize", "html.escape", "allowlist", "shlex.quote"}

def taint_demo(src: str, sink: str, via=()):
    """Return ('VULNERABLE'|'safe', explanation) for a data-flow from src to sink."""
    if src not in UNTRUSTED_SOURCES:
        return ("safe", f"{src!r} is trusted input; no taint introduced")
    if any(step in SANITIZERS for step in via):
        used = [s for s in via if s in SANITIZERS]
        return ("safe", f"tainted data sanitized by {used} before {sink!r}")
    if sink in DANGEROUS_SINKS:
        return ("VULNERABLE", f"untrusted {src!r} reaches {sink!r} unsanitized")
    return ("safe", f"{sink!r} is not a dangerous sink")

if __name__ == "__main__":
    cases = [
        ("request.args", "db.execute", ()),                    # the §13.2 bug
        ("request.args", "db.execute", ("parameterize",)),     # the §13.2 fix
        ("request.form", "innerHTML", ()),                     # the §13.3 DOM bug
        ("request.form", "innerHTML", ("html.escape",)),       # encoded -> safe
        ("config.value", "os.system", ()),                     # trusted source
    ]
    for src, sink, via in cases:
        verdict, why = taint_demo(src, sink, via)
        print(f"{verdict:10s} {src:14s} -> {sink:24s} | {why}")

# Expected output (hand-traced):
# VULNERABLE request.args   -> db.execute               | untrusted 'request.args' reaches 'db.execute' unsanitized
# safe       request.args   -> db.execute               | tainted data sanitized by ['parameterize'] before 'db.execute'
# safe       request.form   -> innerHTML                | tainted data sanitized by ['html.escape'] before 'innerHTML'  ...wait

Trace the fourth case by hand to see why the comment above is deliberately interrupted — it is a teaching trap worth catching. The fourth case is ("request.form", "innerHTML", ("html.escape",)): src is an untrusted source, and via contains html.escape, which is in SANITIZERS, so the function returns safe at the sanitizer check. Good — encoding before a DOM write is the correct §13.3 fix. The third case, ("request.form", "innerHTML", ()), has no sanitizer and innerHTML is a dangerous sink, so it returns VULNERABLE. The fifth, ("config.value", ...), is not an untrusted source, so it returns safe immediately. The corrected expected output is therefore:

VULNERABLE request.args   -> db.execute               | untrusted 'request.args' reaches 'db.execute' unsanitized
safe       request.args   -> db.execute               | tainted data sanitized by ['parameterize'] before 'db.execute'
VULNERABLE request.form   -> innerHTML                | untrusted 'request.form' reaches 'innerHTML' unsanitized
safe       request.form   -> innerHTML                | tainted data sanitized by ['html.escape'] before 'innerHTML'
safe       config.value   -> os.system                | 'config.value' is trusted input; no taint introduced

The lesson of the tool is the lesson of the chapter: a vulnerability is a flow — untrusted data reaching a dangerous interpreter without a boundary in between — and every fix is the insertion of that boundary (a sanitizer, a parameter, an encoder). Meridian now has a way to talk about its code reviews in exactly those terms.

Summary

Web application security is the operational defense of the durable attacks atop the OWASP Top 10. Reference-grade recap:

  • Why web attacks endure: web apps exist to turn untrusted stranger-input into action, so every input is a boundary where data can be mistaken for code. The techniques don't age; only the stakes grow. Defend in three layers — code (the only true fix), browser (CSP, SameSite), and edge (WAF).
  • Injection (§13.2): attacker input becomes part of a database/shell command. Fix: parameterized queries (and list-argument APIs for shell) — a structural boundary, not input filtering. Allowlist identifiers you can't parameterize. Detect: SQL/shell metacharacters in parameters; HTTP 500 spikes.
  • XSS (§13.3): attacker input becomes script in another user's browser. Types: stored (saved, served to many — worst), reflected (echoed from a request, needs a lure), DOM-based (client-side, often invisible to server logs). Fix: context-aware output encoding by default (auto-escaping templates) + safe DOM sinks; CSP (script-src 'self', no 'unsafe-inline') as the defense-in-depth net. Detect: script-like values in inputs; CSP violation reports for DOM XSS.
  • CSRF (§13.4): the browser's automatic cookie sends an authenticated-but-unintended request. Fix: anti-CSRF tokens (works because the same-origin policy blocks the attacker from reading the token) + SameSite cookies. Detect: rejected-token spikes; cross-site Origin/Referer.
  • SSRF (§13.4): the server is tricked into an outbound request to an attacker-chosen (often internal) destination — cloud metadata theft is the worst case. Fix: allowlist destinations, block private/loopback/link-local (re-check after DNS and redirects), and egress-filter at the network. Detect: user-influenced fetches resolving to internal IPs; app server reaching 169.254.169.254.
  • Session & auth flaws (§13.5): session fixation (fixed by rotating the session ID at login); plus HttpOnly/Secure/SameSite cookie flags, high-entropy IDs, real expiration, and server-side logout/invalidation.
  • WAF & detection (§13.6): a WAF is defense in depth and telemetry (virtual patching, blocking the noisy majority) but never a substitute for fixing the code. Hunt web attacks across access logs, WAF logs, and application logs — each catches what the others miss.

Spaced Review

Retrieval practice revisiting Chapter 12 (application security) and Chapter 9 (web/DNS/email). Answer without scrolling up.

  1. (Ch.12) This chapter insisted that input filtering/sanitization is not the fix for SQL injection. Which two Chapter 12 secure-coding principles do provide the durable fixes for injection and XSS respectively, and why are they more reliable than blocklisting bad characters?
  2. (Ch.9) Chapter 9 set Meridian's HTTP-security-header baseline, including Content-Security-Policy. What does the specific directive script-src 'self' (with no 'unsafe-inline') accomplish against XSS that simply "having a CSP" does not?
  3. (Ch.12 / this chapter) Chapter 12 introduced SAST. How does this chapter's taint_demo model the way SAST reasons about an injection vulnerability — in terms of source, sink, and sanitizer?
  4. (Ch.9) A reflected-XSS attack typically arrives as a crafted link in a phishing email. Connect this to Chapter 9's email threats: which email-authentication controls there would, and would not, blunt such a lure, and why?
Answers 1. **Input validation/output encoding** and **parameterization/secure APIs** (Chapter 12's secure-coding patterns): parameterized queries fix injection by separating command from data structurally; output encoding fixes XSS by rendering data as inert text in its context. Both are reliable because they remove the bug *by construction*, whereas blocklisting must anticipate every dangerous character and context and inevitably misses one. 2. `script-src 'self'` with no `'unsafe-inline'` makes the browser refuse to execute *inline* scripts and run only scripts from your own origin — which blocks the injected `