Back to Cheat Sheets

๐Ÿงน Validate all Input & Handle Exceptions

OWASP Top 10 Proactive Controls (2024) - C3

CORE CONTROL

๐Ÿ“‹ What Is This Control?

Validate all Input & Handle Exceptions is a defensive discipline with three tightly related parts: (1) validate all input at the boundary (allow-list, server-side, typed); (2) neutralise at the sink - parameterise queries and apply context-aware output encoding so data is treated as data, never code; and (3) handle exceptions safely - fail closed, return generic errors, and log detail privately. The central rule: validation reduces risk but is not a substitute for output encoding or parameterised queries - you need both.

C3 Proactive Control
Validation Focus Area
OWASP 2024 Proactive Controls

๐ŸŽฏ What It Prevents

  • SQL / NoSQL Injection: Attacker text entering the command structure
  • Cross-Site Scripting (XSS): Injected markup rendered as active script
  • OS Command Injection: Shell metacharacters in a spawned command
  • LDAP Injection: Unescaped filter metacharacters
  • XXE: XML parsers resolving external entities and DTDs
  • Insecure Deserialization: Native deserializers building arbitrary objects (RCE)
  • Path Traversal & Unsafe Uploads: Escaping the intended directory
  • Info Disclosure / Fail-Open: Stack traces leaking internals; errors granting access

๐Ÿงญ How It Works

1. Canonicalise - decode/normalise to a single form
โ†“
2. Validate - allow-list, server-side, syntactic + semantic
โ†“
3. Neutralise at the sink - parameterise queries, encode output
โ†“
4. Configure parsers safely - disable XXE, avoid native deserialize
โ†“
5. On error: fail closed, generic message out, detail to logs

โŒ Without This Control

from flask import Flask, request, jsonify import sqlite3, pickle, subprocess app = Flask(__name__) app.config["DEBUG"] = True โ† interactive debugger reachable @app.route("/user") def user(): name = request.args["name"] โ† no validation db = sqlite3.connect("app.db") # SQL injection: input concatenated into the query row = db.execute("SELECT * FROM users WHERE name = '" + name + "'").fetchone() # XSS: input reflected into HTML with no encoding return "<div>Hello, " + name + "</div>" @app.route("/ping") def ping(): host = request.args["host"] # Command injection: shell interprets ; && | $() return subprocess.run("ping -c 1 " + host, shell=True, capture_output=True).stdout @app.route("/load", methods=["POST"]) def load(): return str(pickle.loads(request.data)) โ† unsafe deserialization โ†’ RCE

โœ… Applying the Control

import re, uuid, logging, subprocess, json from flask import Flask, request, jsonify, abort from markupsafe import Markup app = Flask(__name__) app.config["DEBUG"] = False # no debugger in production log = logging.getLogger("app") NAME_RE = re.compile(r"\A[A-Za-z][A-Za-z '\-]{1,49}\Z") # allow-list, anchored @app.route("/user") def user(): name = request.args.get("name", "") if not NAME_RE.fullmatch(name): # 1) validate (server-side, allow-list) abort(400, "invalid name") # 2a) parameterised query: data sent separately from the command row = get_db().execute("SELECT * FROM users WHERE name = ?", (name,)).fetchone() # 2b) context-aware output encoding: input rendered as inert text return Markup("<div>Hello, {}</div>").format(name) # auto-escapes @app.route("/ping") def ping(): host = request.args.get("host", "") if not re.fullmatch(r"[A-Za-z0-9.\-]{1,253}", host): # validate host abort(400, "invalid host") # argument array + shell=False: no shell to interpret metacharacters return subprocess.run(["ping", "-c", "1", host], shell=False, timeout=5).stdout @app.errorhandler(Exception) def boom(e): error_id = uuid.uuid4().hex log.exception("unhandled error id=%s", error_id) # detail to logs only return jsonify(error="Something went wrong", error_id=error_id), 500 # generic

โœ“ Implementation Checklist

  • Validate with an allow-list, server-side, syntactic + semantic, typed/schema-checked
  • Canonicalise/normalise first, then validate the canonical form
  • Anchor every regex and bound its length
  • Parameterise SQL/NoSQL queries; allow-list identifiers you cannot bind
  • Apply context-aware output encoding; use auto-escaping templates
  • Sanitise rich HTML with a vetted library (e.g. DOMPurify)
  • Run commands via argument arrays, no shell, validated input
  • Disable DTDs and external entities on XML parsers
  • Avoid native deserialization; prefer data-only formats + schema
  • Confine file paths to a base dir; validate uploads by content
  • Fail closed on error; return generic errors, log privately; debug off in prod

๐Ÿ” Tools & Techniques

Validation & Safe Sinks:

Pydantic Zod JSON Schema DOMPurify OWASP Java Encoder Prepared Statements defusedxml

Testing & Scanning:

Semgrep OWASP ZAP Burp Suite CodeQL

How to Verify:

  • Send injection probes (' OR 1=1, <script>, ; id) and confirm 400/inert output
  • Call the API directly (bypass the client) to test server-side validation
  • Post XML with an external entity and confirm it is not resolved
  • Trigger an error and confirm no stack trace reaches the client
  • Confirm a security check that throws denies rather than allows

๐ŸŒ Why It Matters

  • SQL injection breaches: String-concatenated queries have driven some of the largest data breaches on record. The durable fix is parameterised queries, not cleverer input filtering.
  • Stored and reflected XSS: User content rendered without context-aware encoding runs script in victims' browsers - stealing sessions and acting as the victim. Auto-escaping templates shut this down.
  • XXE in XML processors: Parsers with external-entity resolution enabled have been used to read local files and reach internal systems (SSRF). Disabling entities and DTDs is the fix.
  • Unsafe deserialization: Native, type-permissive deserializers have repeatedly led to remote code execution. Data-only formats with a strict schema remove the sink.
  • Verbose errors as reconnaissance: Stack traces have handed attackers versions, paths, and schema names. Generic errors plus private logging deny that free recon.

These are documented incident classes - no fabricated CVE numbers or statistics. The same missing control sits behind all of them.

๐Ÿ“Œ Quick Tips

  • DO NOT treat input validation as a substitute for encoding/parameterising
  • DO NOT build SQL by concatenation or run shell=True with input
  • DO NOT native-deserialize untrusted data or return stack traces
  • DO validate with allow-lists on the server, canonicalising first
  • DO parameterise queries and encode output for the exact context
  • DO disable XXE and fail closed on any security check
  • DO log detail privately and turn debug off in production