Application security is upstream of the browser. Before a byte arrives at TLS, the request handler has already decided how to interpret strings the user controls — and every place a string meets a grammar is a chance for the attacker to supply part of the grammar instead of the data. Three attacks dominate the real world: SQL injection (string meets SQL), XSS (string meets HTML/JS), and CSRF (browser acts on the user's session against their will).
SQL injection: the string becomes a query
The vulnerability is conflation. If you compose a query with f-string interpolation, the boundary between "query" and "data" dissolves:
username = request.form["username"]
cur.execute(f"SELECT * FROM users WHERE username = '{username}'")An attacker who enters ' OR '1'='1 produces WHERE username = '' OR '1'='1' — always true, returning the first user row, commonly an admin. Worse still: '; DROP TABLE users;-- turns your one-liner into multiple statements. The fix is not to filter strings; it is to remove the ambiguity by treating the input as data, not as SQL — parameterized queries:
cur.execute("SELECT * FROM users WHERE username = %s", (username,))The driver escapes the value according to the protocol, so nothing the attacker types can ever be parsed as SQL structure. This one habit removes the entire bug class. Retrofitting validation ("allow only letters") on top of string-building is not a fix — it is a whack-a-mole game, and it loses.
XSS: the string becomes HTML and JavaScript
Same conflation, new grammar. If you render user content into the DOM with innerHTML or mark it safe without escaping, an attacker's comment like <img src=x onerror=alert(document.cookie)> executes as script in every reader's browser, in the context of your origin — so it can read and exfiltrate anything that origin can, including session cookies.
The countermeasure is encoding at the boundary: the framework's auto-escaping ({{ var }} in a template, <Text> in React) treats content as data and HTML-irrelevant characters get entity-encoded. The rules that follow naturally:
- Store as data, encode at output. Persistence is fine; trust nothing when rendering.
- Never build HTML by string concatenation with user input. If you must use a sanitizer, treat it as a band-aid, not a foundation.
- Add a Content-Security-Policy
default-src 'self'which blocks the inline script and external script tags that XSS payloads rely on — the second layer that turns a successful exploit into a broken page. A 57-byte payload that plants<script src=//evil>dies against CSPscript-src 'self'.
CSRF: the session acts without consent
CSRF needs no injection at all. The browser is the attacker's agent: if you are logged in to bank.example and visit evil.example, an image tag on the evil page issues a request to your bank carrying your cookies:
<img src="https://bank.example/transfer?to=attacker&amount=1000">The bank sees a legitimate request from a logged-in user with a valid session cookie. Cookies are sent automatically, which is precisely the problem. The first defense is to stop relying on ambient credentials for state-changing requests:
- SameSite cookie attribute.
SameSite=Lax(default in modern browsers) blocks the cross-site request for POST and for cross-site subresources, andSameSite=Strictblocks GET too. This kills the classic<img>attack for cookies. - CSRF tokens. For the remaining surface (top-level navigations, same-site subdomains), include a per-session random token in the form/header and reject requests that lack it. The token is not in the cookie, so the ambient credential is no longer sufficient.
- Check the Origin/Referer on anything meaningful and reject unexpected origins, cheap second layer.
The pattern to remember: a state-changing request should always be authenticated by evidence the attacker cannot cause the client to supply — a token the server issued and the page presents, never a cookie the browser adds automatically.
Connecting them to the code you write
All three share one shape: input is data until an interpretation boundary turns it into code or an action. The defenses are context-specific but follow a single rule — separate trust:
- Query parameters → parameterized SQL (data vs. query).
- User text → output encoding + CSP (data vs. markup).
- Session cookies → SameSite + CSRF token (credential vs. consent).
Key takeaways
- Parameterized queries eliminate SQL injection; do not attempt to "sanitize" your way to safety.
- Encode at output and deploy CSP to make XSS unattractive.
- SameSite + CSRF tokens stop ambient-credential abuse; never let a cookie alone authorize state changes.
- Every fix is the same fix: refuse to let user data be parsed as grammar.
Production cost model and latency numbers
To apply SQL Injection, XSS, and CSRF: The App-Layer Attacks That Still Work effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:
| Operation / Boundary | Typical Latency | Dominant Bottleneck | Production Mitigation |
|---|---|---|---|
| L1 / L2 Cache Access | 1 ns – 4 ns | Register dependency chains & pointer chasing | Contiguous memory layouts, cache-line alignment (64 B), and batch iteration |
| Main Memory (DRAM) Fetch | 60 ns – 100 ns | TLB misses, random heap indirection, NUMA hops | HugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers |
| Kernel Syscall / Context Switch | 1 µs – 5 µs | User-to-kernel mode switch, page-table flush, lock contention | Non-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O |
| NVMe SSD Random Read / fsync | 50 µs – 500 µs | Block I/O queue depth, write amplification, WAL flushes | Group commit, sequential append logs, and bounded buffer-pool pinning |
| Intra-Region RPC Roundtrip | 0.5 ms – 2.5 ms | Serialization, TCP/TLS framing, tail queueing ($L = \lambda W$) | Connection pooling, deadline propagation, and p95 hedged requests |
Failure modes, debugging, and operational checklist
In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on SQL Injection, XSS, and CSRF: The App-Layer Attacks That Still Work, walk through three verification questions:
- What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
- How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized
100 msslowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early. - Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.