Secure Coding I: Defending Against Injection and XSS
CAP, ACID vs BASE, latency numbers, back-of-envelope estimation, single points of failure — the vocabulary every system designer thinks in.
Core Philosophy: You learned to exploit injection and XSS in Phases 2.4 and 2.5. Now you become the person who makes them impossible. Here is the most important realization in all of secure coding: these vulnerabilities are not bad luck or obscure edge cases — they are the predictable result of a specific coding pattern, and replacing that pattern eliminates the entire vulnerability class. You don’t patch injection bug by bug. You write code in which injection cannot occur.
Part 1: The Problem
In 2.4 and 2.5 you saw the two highest-impact web vulnerability classes from the attacker’s side — injection (SQL, command) and XSS — and their shared root cause: untrusted input being treated as code. You also saw, at the end of each page, a preview of the fix. This page is that fix, built properly.
This matters enormously for you specifically. Your motivation for this whole curriculum was that everyone writes code and nobody secures it. Injection and XSS are exactly where that gap shows — and exactly where a developer who understands the fix becomes immediately valuable. The goal is not “know some defenses.” It is to make you a developer in whose code these bugs structurally cannot appear.
Part 2: The Concept — Fix the Root Cause, Not the Symptom
Recall the root cause from 2.4: injection happens when a program builds a command by mixing trusted code with untrusted input as text, so the input can be read as code. XSS (2.5) is the same thing with the browser as interpreter.
There are two fundamentally different ways to respond, and the difference defines good secure coding:
❌ SYMPTOM approach: "block the bad input"
Detect and reject malicious-looking input
(blocklists, filters, stripping "bad" characters).
→ Attackers endlessly find inputs your filter missed.
→ Whack-a-mole, forever, and you lose.
✅ ROOT-CAUSE approach: "make data structurally inert"
Change HOW you build commands and pages so untrusted
input is STRUCTURALLY INCAPABLE of being executed as
code — no matter what it contains.
→ The vulnerability class is eliminated, not patched.
This is the single most important secure-coding principle, and it generalizes far beyond this page: prefer fixes that make a whole class of bug impossible over fixes that try to catch each instance. Blocklists fail because attackers are creative and you cannot enumerate all “bad” input. Structural fixes succeed because they do not care what the input is — they guarantee it is treated as data.
Part 3: Defending Against SQL Injection — Parameterized Queries
The structural fix for SQL injection is the parameterized query (also called a prepared statement). It is the single most important thing in this page — reach for it in your own code from now on, automatically.
How it works. Instead of building a query by gluing user input into a query string, you do two separate things:
- Define the query’s structure first — the actual SQL code — with marked placeholders where data will go.
- Supply the data separately, to fill those placeholders.
Because the structure is fixed before the data is introduced, the database engine knows exactly which part is code and which is data. The user’s input goes into a placeholder and can only ever be data — structurally impossible to be reinterpreted as query syntax, whatever characters it contains.
❌ VULNERABLE: structure and data mixed as one string
query = "...WHERE name = '" + userInput + "'"
→ userInput can break out and become CODE
✅ SAFE: structure defined first, data supplied separately
query = "...WHERE name = ?" ← code, fixed first
data = [ userInput ] ← data, supplied apart
→ userInput goes in the placeholder; it can ONLY be data
The data/code confusion that is SQL injection is eliminated by construction. This is why “use parameterized queries” is universal advice — not a mitigation, a cure.
Two supporting practices:
- Safe ORM / query-builder use. Many frameworks provide an ORM (Object-Relational Mapper) that parameterizes underneath. Used as intended, these are safe — but they often offer a “raw query” escape hatch, and raw queries built from strings are injectable again. Know which mode you are in.
- Least privilege for the database account (a principle since 0.2). The account the app uses should have only the permissions it genuinely needs — so even if injection slips through, the damage is contained (defense in depth, 1.1).
Part 4: Defending Against Command Injection and Other Injection
Command injection (2.4) has the same root cause aimed at the OS shell — and the same shape of fix.
Best defense: do not call the shell with user input at all. Often, invoking a system command with user input is avoidable — a safe library or built-in function does the job directly. The most secure code does not construct shell commands from input in the first place.
Where a system command genuinely is necessary: use the safe APIs your language provides for running commands — ones that take the command and its arguments as separate, distinct parameters, never as one concatenated string. This is the parameterized-query idea again: command (code) and arguments (data) kept structurally separate, so user-supplied arguments cannot become additional commands.
The general rule for all injection — not just SQL and shells, but any interpreter (XML parsers, LDAP, and more):
Whenever untrusted input meets an interpreter, keep the input structurally separate from the commands — use the parameterized / safe-API mechanism that interpreter provides. Never build commands by concatenating strings with untrusted input.
Internalize that sentence and you have the defense for the entire injection family — including ones you have not met yet.
Part 5: Defending Against XSS — Context-Aware Output Encoding
XSS (2.5) is injection into a web page, executed by the browser. Its structural fix is output encoding (output escaping).
How it works. When the application places untrusted data into a web page, it encodes that data so the browser is guaranteed to treat it as text to display, never as markup or script to execute. A character that would otherwise start an HTML tag becomes its harmless display-only representation — visible on the page, never interpreted as code.
The critical subtlety: encoding must be context-aware. The correct encoding depends on where in the page the data lands — HTML body, an HTML attribute, inside JavaScript each have different rules. Encoding for the wrong context can still be unsafe.
The good news for you as a developer: modern frameworks do most of this automatically. Well-built front-end and templating frameworks encode output by context by default — used as intended, much XSS protection is simply built in. The danger zones: explicitly bypassing the framework’s auto-encoding (most have a “render as raw HTML” opt-out — deliberate, dangerous), older or hand-rolled code that does not auto-encode, and DOM-based XSS where unsafe client-side JavaScript writes input into the page (avoid the unsafe DOM operations that do this).
Defense in depth for XSS — layers behind the primary fix (the 1.1 principle):
- Content Security Policy (CSP) — the HTTP response header from 2.5 restricting what scripts a page may run. A strong CSP can contain XSS even if an encoding gap slips through.
HttpOnlycookies — marking session cookiesHttpOnly(2.5/2.6) means JavaScript cannot read them, blunting XSS’s most common goal: cookie theft.- Input validation — see Part 6.
Part 6: Input Validation — The Valuable Second Layer
If the structural fixes above are the cure, input validation is the valuable supporting layer — important, but understood in its correct role.
What it is. Checking that incoming data conforms to what is expected — right type, length, format, range — and rejecting what does not. An email field should contain something email-shaped; an age should be a number in a sensible range.
The crucial framing — validation supports, it does not replace. Input validation alone does not reliably stop injection or XSS, because plenty of valid-looking input can still be malicious in the wrong context. Parameterized queries and output encoding are the real fixes. Validation is defense in depth on top of them — and genuinely valuable in that role: it reduces attack surface and catches malformed input early.
Two principles for doing it well:
- Allowlist over blocklist (the recurring 2.8 principle). Define what is allowed and accept only that — far more reliable than enumerating everything disallowed.
- Validate on the server. Client-side validation is for user convenience only — the attacker bypasses the browser (the 2.7 lesson). Security validation runs server-side.
LAYER 1 (the cure): parameterized queries / safe APIs
context-aware output encoding
→ vuln class structurally impossible
LAYER 2 (defense in server-side input validation (allowlist)
depth): → catches malformed input early
LAYER 3 (containment): CSP, HttpOnly cookies, DB least privilege
→ limits damage if something slips through
🔑 The deep lesson: secure coding is not “remember to sanitize.” It is choosing constructions in which the vulnerability cannot exist — parameterized queries, safe APIs, framework auto-encoding — then layering validation and containment on top. Fix the class, not the instance.
📓 Key Terms
| Term | Plain meaning |
|---|---|
| Secure coding | Writing code so vulnerabilities cannot occur, by construction. |
| Root-cause fix | A fix that eliminates a whole vulnerability class, vs patching instances. |
| Parameterized query / prepared statement | A query with code structure fixed first and data supplied separately — the SQL injection cure. |
| ORM | Object-Relational Mapper — a framework database layer, parameterized when used properly. |
| Output encoding / escaping | Converting data so a browser shows it as text, never code — the XSS cure. |
| Context-aware encoding | Encoding correctly for where in the page the data is placed. |
| Input validation | Checking input conforms to expectations; a defense-in-depth layer. |
| Allowlist | Permitting only known-good input (preferred over blocklisting). |
🧪 Hands-On Lab
Use the deliberately vulnerable apps from Phase 2 (most ship vulnerable + fixed versions) and small apps of your own. The goal is building and verifying defenses.
Task 1 — Revisit your Phase 2 findings. Open your notes from 2.4 and 2.5. You exploited SQL injection, command injection, and XSS. You will now defend each.
Task 2 — Fix SQL injection with parameterization. Take a SQLi bug you exploited in 2.4. Rewrite the vulnerable query as a parameterized query. Re-run your exact 2.4 attack against the fixed version — confirm it now fails. Experience the fix holding.
Task 3 — Fix command injection. Take the command-injection scenario from 2.4. Rewrite it to avoid the shell entirely, or to use a safe API separating command from arguments. Re-attack; confirm it fails.
Task 4 — Fix XSS with output encoding. Take an XSS bug from 2.5. Apply context-appropriate output encoding (or use a framework’s auto-encoding correctly). Re-run your 2.5 payload; confirm it now renders as harmless visible text.
Task 5 — See framework auto-encoding. In a modern web framework, write a small page that displays user input. Observe auto-encoding by default. Then find the “raw HTML” opt-out and see how using it reintroduces XSS.
Task 6 — Add defense-in-depth layers. For a small app, add a CSP header and HttpOnly on session cookies; add server-side allowlist validation to a field. See how these layer on top of the structural fixes.
Task 7 — Write a secure-coding note. In Notion, create “Secure Coding Patterns.” For injection and XSS, write the root-cause fix, supporting layers, and a before/after. Reference material — and portfolio-relevant (Phase 7).
⚠️ Common Mistakes
- Relying on blocklists / “sanitizing bad characters.” Attackers endlessly bypass them. Use structural fixes that make input incapable of being code.
- Treating input validation as the main defense. Validation is defense in depth; it does not replace parameterized queries and output encoding.
- Using an ORM’s raw-query escape hatch carelessly. ORMs are safe used as intended; raw string queries are injectable again.
- Bypassing framework auto-encoding. The “render as raw HTML” opt-out reintroduces XSS. Use only with fully trusted content.
- Encoding for the wrong context. Output encoding must match where the data lands. Context-aware, or still unsafe.
- Validating only on the client. The attacker bypasses the browser. Security validation runs server-side.
✅ Recap & What’s Next
- Secure coding fixes the root cause: choose constructions where the vulnerability is structurally impossible, rather than blocking “bad” input.
- Parameterized queries cure SQL injection; safe APIs (or avoiding the shell) cure command injection; context-aware output encoding cures XSS — frameworks automate much of this.
- Input validation (server-side, allowlist), CSP,
HttpOnly, and least privilege are defense-in-depth layers on top of the structural cures.
Next (4.2): We continue secure coding with the other major Phase 2 attacks — the authentication, access-control, and request-forgery bugs from 2.6–2.8 — which are design failures needing correct patterns.
⁂ Back to all modules