All chapters in this track
- 01Chapter 1: Why Coding Standards Matter: Readability, Onboarding, Defects, Consistency at Scale
- 02Chapter 2: Naming Conventions
- 03Chapter 3: Formatting and Style
- 04Chapter 4: Comments and Self-Documenting Code
- 05Chapter 5: Error Handling Conventions
- 06Chapter 6: Code and Project Organization (Files, Modules, Packages)
- 07Chapter 7: Writing Testable Code
- 08Chapter 8: Commit Hygiene: Atomic Commits, Messages, PR-Sized Changes
- 09Chapter 9: Language-Specific Idioms and Anti-Patterns Across TypeScript and Python
- 10Chapter 10: Security-Conscious Basics: Input Validation, Secrets Handling, Injection Risks
- 11Chapter 11: Enforcing Standards Automatically: Linters, Formatters, CI Gates
- 12Chapter 12: Creating, Documenting, and Evolving a Team Style Guide (Exceptions and Legacy Code)
- 13Chapter 13: Worked Example: Refactoring a Messy Function to Standard
- 14Chapter 14: Worked Example: Drafting a Style Guide Section From Scratch
- 15Chapter 15: Common Interview Questions About Coding Standards and Code Quality
Chapter 10 of 15
Chapter 10: Security-Conscious Basics: Input Validation, Secrets Handling, Injection Risks
Why this matters
Security vulnerabilities are frequently framed as a specialist concern separate from everyday coding standards — but the large majority of real-world security incidents trace back to a small number of well-understood, entirely preventable coding mistakes, not exotic, sophisticated attacks. Treating these basics as a standard coding practice, rather than a specialist add-on, prevents most of the actual, common vulnerability classes engineers will ever encounter.
Never trust input: validate at every boundary
[BEGINNER] The single foundational principle underlying everything in this chapter: any data crossing a trust boundary (user input, an API request, a file upload, even data from another internal service) must be validated before use — trusting it implicitly is the root cause of most vulnerabilities covered below.
// Anti-pattern: trusting input directly, with no validation at all
function updateUserAge(req: Request) {
user.age = req.body.age; // could be a negative number, a string, or malicious content
}
// Idiomatic: validate at the boundary, before the value enters the system
function updateUserAge(req: Request) {
const age = Number(req.body.age);
if (!Number.isInteger(age) || age < 0 || age > 150) {
throw new ValidationError("age", "must be an integer between 0 and 150");
}
user.age = age;
}
SQL injection: the classic, still-common example
[BEGINNER] SQL injection occurs when untrusted input is concatenated directly into a SQL query string, letting an attacker inject their own SQL logic instead of the intended data value.
# VULNERABLE: user input concatenated directly into the query string
def get_user(username):
query = f"SELECT * FROM users WHERE username = '{username}'"
return db.execute(query)
# An attacker submitting username = "' OR '1'='1" turns the query into:
# SELECT * FROM users WHERE username = '' OR '1'='1'
# ...which matches EVERY row, potentially returning every user's data.
# SAFE: parameterized queries -- the database driver handles escaping correctly,
# treating the input strictly as DATA, never as executable SQL syntax.
def get_user(username):
query = "SELECT * FROM users WHERE username = %s"
return db.execute(query, (username,))
[SENIOR INSIGHT] The reason parameterized queries are the only acceptable defense (not "carefully escaping special characters yourself") is that manual escaping is extremely easy to get subtly wrong — missing an edge case, an unusual encoding, or a database-specific escaping quirk — while a parameterized query structurally separates the query's fixed structure from its data values at the protocol level, making injection through the data channel structurally impossible rather than merely unlikely. This is the exact "structural prevention over convention" theme running through this entire book series, applied to its highest-stakes domain.
Cross-site scripting (XSS): the same principle, applied to HTML output
[BEGINNER] XSS occurs when untrusted input is rendered directly into HTML/a webpage without escaping, letting an attacker inject their own executable script.
// VULNERABLE: user-supplied content rendered directly, unescaped
element.innerHTML = `<div>${userComment}</div>`;
// If userComment contains "<script>stealCookies()</script>", it EXECUTES in
// every other user's browser who views this page.
// SAFE: use APIs that treat content as text, not HTML, by default
element.textContent = userComment; // never interpreted as HTML/script
// Or, when HTML rendering IS needed, use a library that explicitly sanitizes:
element.innerHTML = sanitizeHtml(userComment); // strips dangerous tags/attributes
Most modern frontend frameworks (React, Vue, and similar) escape rendered content by default, which is a significant, deliberate security improvement over manually constructing HTML strings — but this default protection can be bypassed by explicit "raw HTML" escape hatches (React's dangerouslySetInnerHTML, deliberately named to signal the risk), which should be treated as a serious, reviewed exception, never a casual convenience.
Secrets handling: never in source code
[BEGINNER] API keys, database passwords, and other secrets must never be committed to source control, hard-coded in application code, or logged — a secret in a git repository's history remains recoverable indefinitely, even after being "removed" in a later commit, since git history retains every prior version by design.
// NEVER do this -- the secret is now permanently in git history, discoverable
// by anyone with repository access, past or future, even after this line is removed:
const apiKey = "sk_live_abc123xyz789";
// Correct: load secrets from environment variables or a dedicated secrets manager,
// never hard-coded or committed:
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error("API_KEY environment variable is required");
}
[IN PRACTICE] If a secret is ever accidentally committed, rotating it (generating a new one and invalidating the old) is the only real fix — removing it from the latest commit is insufficient, since it remains permanently recoverable from git history; treat any committed secret as compromised the moment it's pushed, regardless of how quickly it's "removed" afterward.
Common mistakes
- Trusting any input crossing a trust boundary without validation, whether from a user, an external API, or even another internal service — validation should happen at every boundary, not just the outermost one.
- Building SQL queries via string concatenation with user input, rather than using parameterized queries — this remains one of the most common, most damaging, and most entirely preventable vulnerability classes in real production systems.
- Rendering untrusted content as raw HTML without escaping or sanitization, or using a framework's "raw HTML" escape hatch casually rather than as a rare, carefully-reviewed exception.
- Committing a secret to source control and believing removing it in a later commit is sufficient — the secret remains permanently recoverable from git history; the only real fix is rotating the secret.
Checklist: security-conscious coding basics
- Validate every piece of data crossing a trust boundary — user input, API request bodies, file uploads, and data from other internal services alike.
- Always use parameterized queries for any SQL constructed with variable data — never string concatenation or manual escaping.
- Rely on your framework's default output escaping for rendering user content, and treat any "raw HTML" escape hatch as a rare, explicitly-reviewed exception, never a casual convenience.
- Never hard-code or commit secrets — load them from environment variables or a dedicated secrets manager, and rotate immediately (not just remove) any secret that's ever accidentally committed.
Discussion / interview questions
-
"Why are parameterized queries the correct defense against SQL injection, rather than manually escaping special characters?" Strong answer: Manual escaping is easy to get subtly wrong (missed edge cases, encoding quirks, database-specific escaping rules); a parameterized query structurally separates the query's fixed structure from its data at the protocol level, making injection through the data channel structurally impossible rather than merely unlikely — the same "structural prevention over convention" principle from encapsulation, applied to security.
-
"What is XSS, and how do modern frontend frameworks help prevent it by default?" Strong answer: Cross-site scripting occurs when untrusted input is rendered as executable HTML/script rather than plain text; modern frameworks (React, Vue) escape rendered content by default, treating it as text unless an explicit, clearly-named escape hatch (like
dangerouslySetInnerHTML) is used — a deliberate design choice making the safe behavior the default. -
"If a secret is accidentally committed to a git repository and then removed in the next commit, is the risk resolved?" Strong answer: No — git history retains every prior version of a file, so the secret remains permanently recoverable by anyone with repository access; the only real fix is rotating the secret (invalidating the old one and generating a new one), not just removing it from the latest commit.
-
"Why should input validation happen at every trust boundary, not just the outermost user-facing one?" Strong answer: Data from an internal service, a file upload, or any other source crossing into a new context can still be malformed, unexpected, or (in a compromised-service scenario) malicious — validating only at the outermost boundary assumes every internal source is inherently trustworthy, which isn't a safe assumption in a real, multi-component system.
Chapter summary / cheat sheet
- Never trust data crossing a trust boundary — validate at every boundary, not just the outermost one.
- SQL injection: always use parameterized queries, never string concatenation with user input — the only structurally safe defense.
- XSS: rely on a framework's default output escaping; treat "raw HTML" escape hatches as rare, reviewed exceptions.
- Secrets: never hard-code or commit them — load from environment variables/secrets managers, and rotate (never just remove) any secret that's ever accidentally committed, since git history retains it permanently.
- Most real-world security incidents trace back to these well-understood, entirely preventable basics, not sophisticated novel attacks — treating them as standard coding practice prevents the large majority of actual risk.