Why this module exists. The single highest-leverage developer education is the principle “structure separates code from data.” Input validation and output encoding operationalise that principle. This module is the practitioner’s reference.
The principle — structure separates code from data
Injection vulnerabilities exist because data is interpreted as code by some downstream parser — SQL parser, HTML parser, shell parser, XML parser, LDAP parser. The defence is two-layered:
- Input validation: reject data that does not match expected structure.
- Output encoding: render data in a way the downstream parser cannot interpret as structure.
Both are needed. Validation alone misses; encoding alone is brittle.
Input validation — the patterns
- Allowlist over denylist. Define what the value can be (regex of valid characters; bounded length; enumerated values), not what it cannot. Denylists miss the case you did not think of.
- Validate at the trust boundary. Validate where untrusted data enters trusted code — typically the API handler. Subsequent code can then trust the value.
- Reject, don’t sanitise. Modifying the input to “make it safe” is brittle and breaks legitimate uses. Reject the request; let the caller fix it.
- Type validation first. If the field is supposed to be an integer, parse it as integer; if parsing fails, reject.
Output encoding — context-specific
The encoding depends on where the value lands:
| Context | Encoding |
|---|---|
| HTML body | HTML entity encoding (<, >, &, ") |
| HTML attribute | Attribute encoding (more aggressive than body) |
| JavaScript string literal | JavaScript escaping; ideally serialise as JSON |
| URL query parameter | URL percent-encoding |
| CSS value | CSS escaping |
| SQL query | Parameterised queries (NOT string concatenation) |
| Shell command | Use API that accepts argument arrays, not shell strings |
SQL injection — the canonical example
# VULNERABLE — string concatenation
query = "SELECT * FROM users WHERE email = '" + email + "'"
db.execute(query)
# Input email = "x' OR '1'='1" → SELECT * FROM users WHERE email = 'x' OR '1'='1'
# SAFE — parameterised query
query = "SELECT * FROM users WHERE email = %s"
db.execute(query, (email,))
# The database treats email as data; SQL injection impossible
# SAFE — ORM (which uses parameterised queries internally)
User.objects.filter(email=email)
Every modern language has parameterised query support. Every modern ORM uses it. The vulnerability persists because of (a) dynamic SQL with string concatenation, (b) ORM bypass into raw SQL when ORM is felt to be too restrictive.
XSS — the encoding example
<!-- VULNERABLE — raw template substitution -->
<h1>Welcome, {{ user_name }}</h1>
<!-- If user_name = "<script>alert(1)</script>", the browser executes the script -->
<!-- SAFE — most templating engines auto-escape by default -->
<!-- Jinja2: {{ user_name }} is auto-HTML-encoded -->
<!-- React: {userName} is auto-HTML-encoded -->
<!-- Vue: {{ userName }} is auto-HTML-encoded -->
<!-- DANGEROUS — explicitly disabling auto-escape -->
{{ user_name | safe }} <!-- Jinja2 -->
{{ trusted_html }} <!-- only when source is provably safe -->
<div dangerouslySetInnerHTML={...} /> <!-- React -->
The pattern in modern XSS: developer hits a case where auto-escape produces wrong output, switches to “safe” mode, forgets to manually sanitise. The audit pattern: grep for explicit unsafe-render annotations.
Command injection
# VULNERABLE — shell string composition
os.system("convert " + user_file + " out.png")
# user_file = "in.jpg; rm -rf /" → command injection
# SAFE — argument array, no shell
subprocess.run(["convert", user_file, "out.png"], check=True)
# No shell parsing; user_file is a single argument
# SAFE — if shell needed, escape arguments
import shlex
subprocess.run("convert " + shlex.quote(user_file) + " out.png", shell=True, check=True)
# But prefer argument array
XXE — the often-missed one
XML External Entity injection — parsing untrusted XML with external-entity resolution enabled. Allows file read, SSRF, denial of service.
# Python defusedxml — disable XXE by default
from defusedxml import ElementTree
tree = ElementTree.parse(untrusted_xml)
# Safe; external entities not resolved
# Java — explicit hardening of DocumentBuilderFactory
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
The “trust boundary” question
The most common failure is treating internal calls as trusted when they should not be. Microservice A calls microservice B. Service B may assume A validated; A assumes B will. Both are wrong if the design does not state it explicitly.
The defensive pattern: every service validates its own inputs, even from internal callers. The cost is duplicate validation; the benefit is correctness when (not if) a caller is compromised.
Common framework defaults — are they on?
- Django: auto-escapes by default in templates; uses parameterised queries; XXE-safe XML parsing.
- Flask + Jinja2: auto-escapes; you need to pick parameterised SQL library.
- React: auto-escapes by default; dangerouslySetInnerHTML is opt-in.
- Spring Boot: Hibernate ORM uses parameterised queries; depends on developer using ORM not raw JDBC.
- Express + EJS: depends on EJS escape syntax; explicit unescape is easy.
Audit a project’s framework configurations periodically; defaults that were correct at v1 may have been changed.
Key takeaways
- Principle: separate code from data; validate at trust boundary, encode at output context.
- Input validation: allowlist, type-validate, reject not sanitise.
- Output encoding: context-specific — HTML body, attribute, JS, URL, CSS, SQL, shell each different.
- SQL injection: parameterised queries always; ORM raw-SQL escape hatch is risky.
- XSS: auto-escape default in modern templates; audit explicit unsafe-render usage.
- Command injection: argument arrays, no shell strings.
- XXE: explicitly harden XML parser configuration; defusedxml / equivalent.
Get a VAPT scoping call
Senior practitioner-led VAPT — not a checklist run by juniors. CVSS-scored findings, free retest, attestation letter. India's SMBs and SaaS teams.