Home › Security › Application security
guide low

Input validation and injection

Problem Anyone handling data from requests, files, queues, webhooks or LLM output needs to avoid it being interpreted as code in SQL, shells, HTML, paths or templates. This guide answers how to treat untrusted input safely.

Keep untrusted data as data: parse it into types at the boundary, and never splice it into code, queries, shells or HTML.

injectionsql-injectionxsscommand-injectionpath-traversalvalidationdeserialization
Overview

Injection happens when untrusted input is spliced into something another interpreter parses: SQL, a shell, HTML, a path, a template. It has compromised telecom, credit-reporting, cloud and gaming systems. The fix is APIs that keep code and data in separate channels (bound parameters, argv arrays, auto-escaping templates), not hand escaping. Second layer: parse input into typed values at the boundary, reject the rest.

Key facts

Use when

  • Any data from requests, files, queues, webhooks, LLM output or other services, in any field (telecom, credit reporting, cloud, gaming: see In production).

Avoid when

  • Never; "internal" inputs become external the day a proxy or partner integration appears.

Deep dive

Decision table

SinkRight APINever
SQL valuesPrepared statements / bound params ($1, ?)format!, f-strings, string concat
SQL identifiers, ORDER BYAllowlist map from user token to fixed SQLQuoting user input as identifier
OS commandsCommand::new(bin).arg(x), subprocess.run([...]) ; -- before user argssh -c, shell=True, system()
HTMLAuto-escaping templates (askama, Jinja2 autoescape, React JSX)innerHTML, dangerouslySetInnerHTML, the safe filter on user data
Rich text from usersSanitizer with allowlist (ammonia, DOMPurify)Regex-based stripping
File pathsGenerated names (UUID) or canonicalize + prefix check; openat2(RESOLVE_BENEATH) / cap-stdJoining user filenames to a directory
URLs from usersParse, allowlist scheme (https), SSRF guardFetching arbitrary URLs from servers
Structured inputTyped deserialization with size limits, deny_unknown_fieldsGeneric object deserialization of untrusted data
XMLParser with DTDs/external entities disabledDefault old parsers (XXE)
LogsStructured logging (JSON fields)Writing raw user strings with newlines

Code

Rust examples (sqlx = { version = "0.8", features = ["postgres", "runtime-tokio"] }, serde).

use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::process::Command;

// 1. Parse at the boundary: types carry the validation forward.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CreateUser {
    pub email: String,
    pub display_name: String,
}

pub struct Email(String);
impl Email {
    pub fn parse(s: &str) -> Result<Self, &'static str> {
        let s = s.trim();
        if s.len() > 254 || !s.contains('@') || s.chars().any(char::is_control) {
            return Err("invalid email");
        }
        Ok(Email(s.to_ascii_lowercase()))
    }
}

// 2. SQL: values bound, sort column from an allowlist.
pub async fn list_users(pool: &sqlx::PgPool, tenant: i64, sort: &str)
    -> sqlx::Result<Vec<(i64, String)>> {
    let order = match sort {
        "name" => "display_name",
        "created" => "created_at DESC",
        _ => "id",
    };
    let sql = format!("SELECT id, display_name FROM users WHERE tenant_id = $1 ORDER BY {order} LIMIT 100");
    sqlx::query_as(&sql).bind(tenant).fetch_all(pool).await
}

// 3. Processes: no shell, args separate, "--" ends option parsing
//    so a pattern like "-r" or "--include=..." is not read as a flag.
pub fn search_file(pattern: &str, file: &Path) -> std::io::Result<String> {
    let out = Command::new("/usr/bin/grep")
        .arg("-F")             // fixed string, no regex surprises
        .arg("--")
        .arg(pattern)
        .arg(file)
        .output()?;
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}

// 4. Paths: resolve and confine.
pub fn confined(root: &Path, user_rel: &str) -> Option<PathBuf> {
    let root = root.canonicalize().ok()?;
    let p = root.join(user_rel).canonicalize().ok()?; // resolves "..", symlinks; file must exist
    p.starts_with(&root).then_some(p)
}

For tools that don't support --, pass user paths as absolute paths so they can't start with -.

HTML output with askama auto-escapes {{ name }}; put user data only in text and quoted attribute contexts, never into <script> blocks or href without scheme checks.

Efficiency

Cost: validation is cheap CPU next to the I/O it guards; the real costs are hostile input sizes and wasted work on requests that will be rejected anyway. Bound by parsing and I/O, not compute.

Levers:

  • Reject early: body, depth and array caps before parsing, cheap checks (length, type) before regexes or lookups — avoids all downstream work — any public endpoint.
  • Bound parameters — let the driver reuse prepared statements; sqlx caches them per connection (statement_cache_capacity) — every query.
  • Build-time templates — askama compiles templates to Rust, so auto-escaping adds no runtime parsing — server-rendered HTML.
  • Linear-time regex — Rust's regex guarantees search time linear in the input, so ReDoS cannot burn a core — any user-supplied pattern or text.
  • Sanitize once on write — store sanitized rich text, re-run only when the policy changes — rich-text content.

Naive → standard → state of the art:

  • Naive: hand-built query strings, backtracking regexes, validation after parsing · exploitable and unbounded work per request
  • Standard: bound parameters, size caps first, escape-on-output · near-zero overhead
  • State of the art: typed parsing at the edge, linear-time regex, sanitize-on-write; plain-text escaping or UUID file names where rich text or user paths are not needed · least work per request

How to validate

  • Validate type, length, range and format; reject, don't "fix".

  • Canonicalize first (Unicode NFC/NFKC, trim, lowercase where semantic), then validate, then use.

  • Cap sizes everywhere: request body (order of 1 MiB default), JSON depth, array length, upload size, regex input length.

  • Regexes on untrusted input: use linear-time engines (Rust regex, RE2) to avoid ReDoS.

  • Treat LLM output as untrusted input: it can carry injection payloads from documents it read.

  • Errors: reject with a generic message and log the detail server-side; never echo raw input into error pages.

Pitfalls

  • Escaping by hand (replace("'", "''")) instead of binding; breaks on encodings and edge cases.
  • ORMs with raw-query escape hatches (.raw(), whereRaw, text()) with interpolated input.
  • Validating on the client only.
  • Blocklists (<script>, ../): bypassed by encodings, case, ..%2f, ....//.
  • canonicalize + check, then opening later: a symlink swapped in between (TOCTOU). Open relative to a directory handle (cap-std) when attackers can write in the tree.
  • Zip/tar extraction without path checks ("zip slip").
  • XSS via javascript: URLs in href, or user data in inline event handlers.
  • Template engines compiling user input as a template (server-side template injection).
  • Header injection via CR/LF in values placed into HTTP headers or emails.

In production

Injection flaws are exploited across fields:

  • Telecom: the 2015 TalkTalk breach used SQL injection in legacy web pages (156,959 customers' data accessed; £400,000 ICO fine).
  • Cloud infrastructure, gaming, consumer services: Log4Shell (user-controlled log strings triggered JNDI lookups) affected AWS, Cloudflare, iCloud, Minecraft: Java Edition and Steam.
  • Credit reporting: the 2017 Equifax breach followed an unpatched Apache Struts flaw (CVE-2017-5638) that let a crafted Content-Type header run commands.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
sqlxRusttransact-rs/sqlxcargo add sqlx————bound parameters, statement cache; Postgres, MySQL, SQLite
askamaRustaskama-rs/askamacargo add askama————compile-time templates, auto-escaping by default
ammoniaRustrust-ammonia/ammoniacargo add ammonia————allowlist HTML sanitizer
cap-stdRustsunfishcode/cap-stdcargo add cap-std————directory-handle file access, no path escape
DOMPurifyJavaScriptcure53/DOMPurifynpm install dompurify————browser-side HTML sanitizer

Sources

  • OWASP, SQL Injection Prevention Cheat Sheet. doc
  • OWASP, Cross Site Scripting Prevention Cheat Sheet. doc
  • OWASP, OS Command Injection Defense Cheat Sheet. doc
  • OWASP, Deserialization Cheat Sheet. doc
  • OWASP, Input Validation Cheat Sheet. doc
  • MITRE, CWE-89, CWE-78, CWE-79, CWE-22 entries. CWE-89 · CWE-78 · CWE-79 · CWE-22
  • King, Parse, don't validate, 2019. post

Builds on

Used by

Systems (10):

Alternatives and related