Module 3 · Input Validation and Output Encoding — Universal Defences

Manish Garg
Manish Garg Associate of (ISC)² · RingSafe
May 14, 2026
5 min read
Read as
100% Free

No signup. No paywall. No catch. One of our 10 most-requested practitioner modules — published in full so anyone can learn for free. We earn through consulting, not by gating knowledge.

See all 10 free modules →

Why this module exists. Input validation and output encoding are the universal defences against the OWASP Top 10 of injection vulnerabilities — SQL injection, XSS, command injection, LDAP injection, XXE. Every framework provides them; every developer claims to use them; many still ship vulnerable code. This module covers the principles and the language-specific patterns.

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

  1. 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.
  2. Validate at the trust boundary. Validate where untrusted data enters trusted code — typically the API handler. Subsequent code can then trust the value.
  3. 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.
  4. 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.
Need a real pentest?

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.

Book VAPT scoping call Replies in 4 working hrs · India-only · Senior consultants