Injection happens when data crosses into an interpreter and is read as code, and no amount of filtering at the front door can know every interpreter the data will reach later. Input validation and output encoding are the two controls that stop it, but they do different jobs at different points in the code, and most injection bugs come from using one where the other belongs. This guide sets out how to design validation rules that hold up, which encoding or parameterisation each interpreter needs (SQL, shell, HTML, JavaScript, URLs, logs, CSV, CI scripts and more), where in the architecture each control should live, and how to roll it out and measure it. It is written for developers, AppSec engineers and reviewers; the deep dives on individual injection classes are linked from each row.
Key takeaways
- Validate for correctness, encode for safety. Validation at the trust boundary checks that data makes sense for the business; encoding or parameterisation at the sink stops it changing the structure of a query, page or command.
- The sink decides the defence. HTML body, HTML attribute, JavaScript string, URL, SQL value, SQL identifier, shell argument and log line each need a different control, and the wrong one can be worse than none.
- Decode once, then validate. ASVS 5.0 V1.1.1 requires input to be decoded into canonical form once, before validation; a second decode downstream reopens everything the validator closed.
- Allowlists for structure, never denylists for safety. Stripping quotes or angle brackets breaks legitimate data and still does not make a query or page safe.
- Parameterise names as well as values. Column names, sort keys and JSON field names that reach SQL need an allowlist mapping, as the 2026 McKinsey Lilli case showed.
Four controls and what each one is for
OWASP's Input Validation Cheat Sheet is direct about the division of labour: use parameterised queries for SQL and context-aware output encoding for cross-site scripting, and check authorisation separately, because a valid identifier does not mean the caller may use it. CWE-20 (Improper Input Validation) reaches the same conclusion through its own example, where a birthday field restricted to digits and dashes still let a payload through, and concludes that encoding combined with validation is the useful protection.
| Control | What it does | Where it runs | What it cannot do |
|---|---|---|---|
| Input validation | Rejects data that does not meet syntax and business rules (type, length, range, format, allowed values) | At every trust boundary, on the server, after decoding and parsing | Make free text safe for every interpreter it will reach |
| Parameterisation | Sends code and data to the interpreter separately (prepared statements, argv arrays, query builders) | At the call to the database, OS or directory | Protect identifiers, operators or keywords that are concatenated in |
| Output encoding / escaping | Transforms data so the target interpreter reads it as literal text | As the last step before the sink, or by the interpreter itself (ASVS V1.1.2) | Protect "dangerous contexts" such as event handlers or eval() |
| Sanitisation | Removes or rewrites unsafe parts of rich content that must keep some markup (HTML, SVG, Markdown) | When rich content must be rendered | Survive later modification of its output |
Validation should come first because rejecting malformed data early keeps it out of storage, logs and downstream systems. Encoding has to be last because only the code that writes to the sink knows which interpreter will read the result.
Designing validation rules that hold up
Validate both syntax (the expected type and format) and semantics (whether the value makes sense, for example an end date after a start date). ASVS 5.0 V2.2.1 asks for positive validation against allowed values, patterns and ranges, applied to all input from Level 2, and V2.2.2 requires it at a trusted service layer, not only in the browser.
| Input type | Rules to enforce | Common gap |
|---|---|---|
| Fixed choices (status, country, sort column) | Exact membership of a server-side set; map the value to an internal constant | Trusting drop-down values because the UI only offers valid ones |
| Numbers and dates | Type, format, minimum and maximum, timezone | Integer overflow and negative quantities (ASVS V1.4.2) |
| Identifiers | Format (UUID, numeric), then an authorisation check on the object | Treating a valid ID as permission to access it |
| Free text (names, comments) | Length, valid encoding, allowed Unicode categories, normalisation policy | Rejecting apostrophes and non-Latin scripts in the name of security |
| Structured objects | Required fields, allowed fields, nested item rules, array bounds | Schemas that list properties but do not reject unknown ones |
| URLs | Parse with one library, then check scheme, host and path against an allowlist | String-prefix checks, and different parsers disagreeing (ASVS V1.5.3) |
| Files | Size, type by content, generated filenames | Trusting the submitted filename and Content-Type |
- Limit before you parse: apply request size limits before buffering, and parser limits such as maximum JSON nesting depth. RFC 8259 explicitly allows implementations to limit size, depth and numbers. A schema check after parsing cannot protect the parser.
- Close the schema: in JSON Schema, set
"additionalProperties": falseand an explicitrequiredlist; listing a property alone neither requires it nor rejects unknown fields. Bind only intended fields to objects to avoid mass assignment. - Decode once, in one place: decode percent-encoding, HTML entities or Base64 according to the protocol, validate the decoded value, and never decode again downstream. CWE-20 points to double decoding (CWE-174) as a way dangerous input slips past an allowlist; CAPEC-267 Leverage Alternate Encoding describes the attacker side.
- Regex with whole-value matching and bounded cost: use full-match APIs such as Python
re.fullmatch, bound length before matching, and avoid patterns with catastrophic backtracking (ASVS V1.3.12). Use a non-backtracking engine or match timeout where available. - Unicode policy: agree one character encoding (UTF-8) across components, reject malformed sequences, and if a field needs normalisation for comparison, apply the same form (for example NFC or NFKC under Unicode Annex #15) before validation, storage and comparison.
- Validate internal sources too: partner feeds, queues, stored records and AI model output cross trust boundaries even when the transport is internal.
- Reject, do not repair: fail the request with a clear error rather than continuing with partially validated or "cleaned" data.
The sink map: matching the defence to the interpreter
This is the core reference. For each place untrusted data ends up, use the defence in the second column; encoding is listed only where parameterisation is not available.
| Sink | Correct defence | Avoid | Standard |
|---|---|---|---|
| SQL, HQL, NoSQL, Cypher values | Parameterised queries, ORM bind variables | String concatenation, manual quote escaping | ASVS V1.2.4; SQL Injection Prevention Guide |
| SQL identifiers (column, table, sort order, JSON keys) | Map user choice to a hard-coded identifier from an allowlist | Passing field names through because values are parameterised | ASVS V1.2.4 |
| OS commands | Avoid the shell; call the program with an argument array; allowlist arguments | Building a command string, shell escaping as the only control | ASVS V1.2.5; Command Injection Guide |
| LDAP | Escape for the specific part (search filter versus distinguished name), use framework encoders | One generic escape for both | ASVS V1.2.6 |
| XPath | Parameterised or precompiled queries | Concatenated expressions | ASVS V1.2.7 |
| HTML body | Template auto-escaping (HTML entity encoding); textContent in the DOM | innerHTML, framework escape hatches such as dangerouslySetInnerHTML | ASVS V1.2.1; Cross-Site Scripting (XSS) Prevention Guide |
| HTML attribute | Quote the value and attribute-encode; keep attribute names fixed | Unquoted attributes; untrusted data in event-handler attributes | ASVS V1.2.1 |
| URL parameter | URL-encode the component (encodeURIComponent()), then attribute-encode the whole URL | Encoding the whole URL once and assuming it is safe in href | ASVS V1.2.2 |
Untrusted URL in href or src | Parse, allow only https: (and http: if needed), then attribute-encode | Blocking javascript: by string match | ASVS V1.2.2 |
| JavaScript string in inline script | An encoder documented for JavaScript inside HTML script; better, pass data via a JSON data attribute | Escaping quotes by hand; any use of eval() | ASVS V1.2.3, V1.3.2 |
| CSS | Strict structural validation of the value (for example a colour pattern), CSS hex encoding | Free text in style attributes | ASVS V1.2.1 |
| HTTP response headers and filenames | Reject CR and LF; encode filenames per RFC 6266 | Raw user values in Location or Content-Disposition | ASVS V5.4.2 |
| Log entries | Structured logging; encode CR, LF and delimiters | Logging raw request bodies or secrets | ASVS V16.4.1; CWE-117 |
| CSV and spreadsheet exports | RFC 4180 quoting plus formula-injection escaping of leading =, +, -, @ | Quoting alone | ASVS V1.2.10 |
| Regular expressions built from input | Escape metacharacters with the language's escape function | Inserting user text into a pattern | ASVS V1.2.9 |
| Template engines | Never build templates from input; pass data as template variables | Rendering user-supplied template strings | ASVS V1.3.7; Server-Side Template Injection (SSTI) Guide |
| XML parsers | Disable external entities and DTDs | Default parser settings on untrusted XML | ASVS V1.5.1; XXE Guide |
| CI/CD inline scripts | Pass untrusted event data through an environment variable or an action input, then quote it | Interpolating PR titles or branch names directly into run: scripts | GitHub Actions security hardening |
| LLM prompts | No encoding neutralises natural language: separate instructions from data and constrain what the model can do | Assuming delimiters or escaping prevent prompt injection | Prompt Injection Guide |
Where encoding belongs in the architecture
- Store raw, encode on output: keep the canonical value in the database and encode for the sink at render time. Data encoded for HTML on the way in is wrong in a CSV export, a JSON API or an email, and gets double-encoded when a template escapes it again.
- Let the interpreter's own API do it: prepared statements,
subprocessargument lists,textContentandsetAttribute()separate data from code without any string encoding. ASVS V1.1.2 accepts encoding "by the interpreter itself" for this reason. - Use auto-escaping templates and audit the escape hatches: most frameworks escape by default; the risk sits in
|safe,raw,Html.Raw(),v-html,bypassSecurityTrustHtmland similar. Inventory them and require review for each new use. - One vetted encoder library per platform: for example the OWASP Java Encoder, or the framework's built-in encoders in .NET and Python templating. CWE-116 (Improper Encoding or Escaping of Output) recommends vetted libraries over hand-written escaping.
- No global input filters: a WAF or middleware that strips characters from every request cannot know the output context and breaks legitimate data. Keep a WAF as detection and virtual patching, not as the control.
- Declare character sets: every response with a body should carry a Content-Type with charset (ASVS V4.1.1), so the browser and the encoder agree on what the bytes mean.
- Sanitise rich content once, with a maintained library: for HTML from editors use a sanitiser such as DOMPurify (ASVS V1.3.1) and do not modify its output afterwards. Details are in the Cross-Site Scripting (XSS) Prevention Guide.
Implementation roadmap
- Map sinks: use SAST to list every database call, process launch, template render, DOM write, log call and export function, and tag each with its interpreter. This is the scope for the rest of the plan.
- Fix the highest-impact sinks first: concatenated SQL and shell commands reachable from unauthenticated endpoints, then escape hatches in templates, then everything else.
- Publish validation contracts: define schemas for every external API (OpenAPI with JSON Schema, Protocol Buffers) with closed objects, bounds and formats, as ASVS V2.1.1 requires documented rules. See the Secure API Design Guide.
- Enforce in report-only mode: run schema validation and new encoders in log-only mode in a pilot ring for one to two weeks, review rejections for false positives (legitimate names, scripts, punctuation), then switch to reject.
- Gate the pipeline: add SAST rules that fail on new concatenated queries, shell string execution, unsafe DOM sinks and template escape hatches; add negative tests for each validation rule (out of range, unknown field, oversize array, near-matching values).
- Roll back safely: keep validation enforcement behind a per-route flag so a false-positive rule can return to report-only without redeploying; never roll back a parameterisation fix.
- Extend to non-web sinks: logs, CSV exports, email, CI workflows and LLM integrations, which scanners usually miss.
Decision guidance
- Reject or sanitise? Reject when the field has a defined format. Sanitise only when users must submit rich content, and then with a library, never a regex.
- Gateway or service validation? Use both: the API gateway enforces size limits and schema shape cheaply, the service enforces semantics and authorisation, which the gateway cannot see.
- Encoding or parameterisation? Parameterise wherever the interpreter offers it. Encoding is the fallback for sinks that are text formats (HTML, CSV, logs).
- Small teams: rely on framework defaults (ORM, auto-escaping templates, schema validation middleware) and a short list of banned APIs in CI. That covers most of the risk at low cost.
- Large organisations: publish an approved library list per language, central schema registries for APIs, and a sink inventory tracked in ASPM tooling, with exceptions approved by AppSec.
Detection and measurement
- Log validation failures as security events: ASVS V16.3.3 expects attempts to bypass controls to be logged. Record the rule, field and source, not the raw value, and encode anything retained (V16.4.1).
- Watch database and interpreter errors: syntax errors from the database driver, template engine or shell in production usually mean a sink received structure it did not expect. In the McKinsey case, field names reflected in database error messages were what exposed the query structure.
- Alert on exploitation follow-on: injection into public applications maps to T1190 Exploit Public-Facing Application; command injection then shows as shells spawned by application processes (T1059).
Metrics:
- Open injection-class findings (CWE-89, CWE-78, CWE-79, CWE-117) by severity and age.
- Percentage of external API operations with a closed, enforced schema.
- Count of template escape hatches and unsafe DOM sinks, trended to zero or to reviewed exceptions.
- Validation rejection rate per route, with false-positive rate from report-only pilots.
- Production interpreter syntax errors per week (database, template, shell).
Framework mapping
| Framework | Reference | What it covers |
|---|---|---|
| OWASP ASVS 5.0 | V1.1.1, V1.1.2, V1.2.1 to V1.2.10, V1.3.1, V1.3.12 | Canonical decoding, encoding as the last step, per-interpreter injection controls, sanitisation, ReDoS |
| OWASP ASVS 5.0 | V2.1.1, V2.2.1, V2.2.2, V16.4.1 | Documented rules, positive validation, server-side enforcement, log encoding |
| MITRE CWE / CAPEC | CWE-20, CWE-116; CAPEC-66, CAPEC-63, CAPEC-88, CAPEC-267 | Weaknesses and attack patterns for validation, encoding, SQL, XSS, OS command injection and alternate encodings |
| NIST SP 800-53 Rev. 5 | SI-10, SI-10(5), SI-15, SA-11, SA-15 | Input validation, approved formats, output filtering, developer testing, development standards |
| NIST CSF 2.0 | PR.PS-06; ID.RA-01 | Secure development practice; finding and recording vulnerabilities |
| ISO/IEC 27001:2022 Annex A | A.8.28; A.8.29 | Secure coding; security testing in development and acceptance |
| MITRE ATT&CK v20 | T1190; T1059 | Exploitation of public-facing applications and resulting execution |
Common failure patterns
- Client-side only validation: browser checks are for usability; anything that calls the API directly bypasses them.
- Denylist "cleaning": removing suspicious characters changes meaning, blocks real names and still leaves the sink unprotected.
- Encoding on input: HTML-encoding at save time produces double-encoded pages and unprotected non-HTML outputs.
- Parameterised values, concatenated names: dynamic column lists, sort keys and filter field names are left out of the bind mechanism.
- Trusting internal or "metadata" fields: branch names, filenames, HTTP headers and partner feeds are attacker-controllable more often than teams assume.
- Logging raw rejected input: creates log injection and puts secrets and personal data into log stores. See the Secure Error Handling and Logging Guide.
Lessons from incidents
- McKinsey Lilli (2026): an unauthenticated endpoint parameterised search values but concatenated JSON field names into SQL; standard scanners did not flag it, and an autonomous agent inferred the query from error messages. See McKinsey AI Platform Hack 2026. Lesson: identifiers need allowlist mapping, and verbose database errors help attackers.
- Ultralytics (2024): a GitHub Actions workflow on
pull_request_targetinserted a pull request's branch name into a shell script, so an attacker-chosen branch name ran commands in a privileged CI context and led to poisoned releases. See Ultralytics PyPI Compromise 2024. Lesson: CI scripts are an injection sink, and the fix is the same as for any shell: pass data as variables, never as code.
Practitioner checklist
- Inventory sinks by interpreter and record the required defence for each from the sink map.
- Replace every concatenated SQL, LDAP, XPath and shell call with parameterised APIs; map identifiers through allowlists.
- Define closed schemas with bounds for all external APIs and enforce them on the server.
- Decode input once, before validation, and use one URL parser and one character encoding across components.
- Keep auto-escaping on, inventory template escape hatches and unsafe DOM sinks, and require review for new ones.
- Store raw values and encode at output with one vetted encoder library per platform.
- Escape logs, CSV exports, response headers and CI workflow inputs, not just web pages.
- Run new validation rules in report-only mode first, with a per-route rollback flag.
- Fail builds on new unsafe sinks and add negative tests for each validation rule.
- Track the five metrics above and review exceptions quarterly.
Standards and references
- OWASP: Application Security Verification Standard 5.0
- OWASP: Input Validation Cheat Sheet
- OWASP: Cross Site Scripting Prevention Cheat Sheet
- OWASP: Injection Prevention Cheat Sheet
- MITRE: CWE-20 Improper Input Validation
- MITRE: CWE-116 Improper Encoding or Escaping of Output
- NIST: SP 800-53 Rev. 5
- GitHub: Security hardening for GitHub Actions
Related NHI Mgmt Group resources: Cross-Site Scripting (XSS) Prevention Guide · SQL Injection Prevention Guide · Secure Deserialisation Guide · Secure Coding Guide · Content Security Policy Guide