Injection Attack Prevention: SQL, NoSQL, OS Command & Template Injection
Injection attacks rank among the highest-severity vulnerabilities in modern web architecture — when untrusted data crosses an interpreter boundary without proper context separation, an attacker can execute arbitrary queries, read sensitive data, or achieve remote code execution within the application process. In polyglot persistence, microservices, and serverless environments, the attack surface spans SQL and NoSQL databases, OS command shells, LDAP directories, XML parsers, and server-side template engines simultaneously. PCI-DSS v4.0 Requirement 6.2.4, SOC 2 CC6.1, and OWASP ASVS V5 all mandate proactive, code-level controls rather than reactive patching. This guide is part of the Vulnerability Patterns & Web Mitigation Strategies section and covers every major interpreter class — from SQL parameterization through SSTI sandboxing — in the depth required to actually ship safe code. Where CSRF token patterns deal with session integrity, and Cross-Site Scripting (XSS) mitigation addresses DOM rendering, injection prevention requires a fundamentally different architectural response: query structure must be permanently separated from user-controlled data at the source.
Threat Anatomy
Injection is not a single vulnerability — it is a class of flaws that share one root cause: an application constructs an executable string by concatenating data with syntax, then passes that string to an interpreter. The interpreter cannot distinguish the developer’s intended syntax from attacker-supplied metacharacters, so it executes both.
Attacker perspective. A classic SQL injection payload like ' OR '1'='1 works because the database parser receives a syntactically valid WHERE clause after string concatenation. An OS command injection payload such as ; cat /etc/passwd appends a shell metacharacter that spawns a second command. An SSTI payload like {{ 7*7 }} (or ${7*7} in EL-based engines) exploits the template engine’s own expression evaluator. The attacker’s goal in each case is the same: promote data to code.
MITRE ATT&CK coverage. Injection maps to T1190 (Exploit Public-Facing Application) for initial access, and T1059 (Command and Scripting Interpreter) for execution in OS command and template injection variants. SQL and NoSQL injection enabling data exfiltration align with T1213 (Data from Information Repositories).
Real-world impact. SQL injection was the leading cause of data breaches in OWASP Top 10 2021 (A03). NoSQL operator injection in MongoDB applications has been used to bypass authentication by injecting $ne: null operators into login queries. SSTI in Jinja2 and Freemarker has enabled full RCE on web application servers, including server-side access to environment variables carrying cloud provider credentials.
For deep-dive implementation of the database tier specifically, see parameterized queries for SQL and NoSQL injection.
Injection Class Reference
| Class | Target Interpreter | Canonical Payload Pattern | Primary Control |
|---|---|---|---|
| SQL Injection | RDBMS query parser | ' OR 1=1-- |
Prepared statements / parameterized bindings |
| NoSQL Operator Injection | MongoDB / Elasticsearch query object | { "$gt": "" } |
Strict schema validation + operator allowlist |
| OS Command Injection | Shell (/bin/sh, cmd.exe) |
; rm -rf / |
Argument arrays, no shell interpolation |
| LDAP Injection | LDAP directory filter parser | *)(uid=*) |
Escaped attribute values, allow-listed characters |
| Server-Side Template Injection | Jinja2, Freemarker, Pebble, Twig | {{ config.items() }} |
Sandboxed execution, auto-escape, no user-sourced templates |
| XML External Entity (XXE) | XML parser | <!ENTITY xxe SYSTEM "file:///etc/passwd"> |
DTD disabled, external entity resolution off |
| XPath Injection | XPath 1.0 / 2.0 processor | ' or '1'='1 |
Parameterized XPath, input allowlisting |
Prerequisites & Scope
Before applying the controls in this guide, verify the following preconditions are in place:
- Dependency inventory: All database drivers, ORM libraries, template engines, and XML parsers are catalogued with pinned versions. Vulnerable versions of
mysql,mongoose,jinja2,libxml2, and equivalent packages are upgraded. - Input ingress map: Every external input surface (HTTP request body, query string, headers, cookie values, file uploads, webhook payloads, inter-service API calls) is documented in a data flow diagram — see attack surface mapping techniques for the methodology.
- Trust boundary definitions: Service accounts, API keys, and database credentials follow least-privilege principles. The application database user does not hold
DROP,TRUNCATE,EXECUTE, or DDL privileges. - Framework assumptions: Examples below cover Node.js with
pg-promise/ Mongoose, Python with SQLAlchemy / Jinja2, and Java with Hibernate JPA. Adapt parameterization syntax to your driver. - Build tooling: A SAST tool (Semgrep, CodeQL, or SonarQube) is integrated into the CI pipeline and can be configured with custom rules for query concatenation patterns.
Mitigation Architecture
The core architectural principle is interpreter boundary isolation: user-controlled data and developer-authored syntax must never be concatenated into a string that is subsequently parsed by an interpreter. The safe path routes data through a binding layer that the interpreter treats as an opaque literal — never as executable syntax.
Vulnerable vs. Hardened: Side-by-Side
| Pattern | Vulnerable | Hardened |
|---|---|---|
| SQL query construction | "SELECT * FROM users WHERE id='" + id + "'" |
db.query("SELECT * FROM users WHERE id=$1", [id]) |
| NoSQL filter | User.find({ username: req.body.username }) (no schema) |
User.find({ username: z.string().parse(req.body.username) }) |
| OS command | exec("ping " + host) |
execFile("/bin/ping", ["-c", "1", host]) |
| Template rendering | env.from_string(user_template).render() |
Pre-compiled template from trusted registry + auto-escape |
| LDAP filter | "(&(uid=" + user + "))" |
"(&(uid=" + escapeLdapFilter(user) + "))" |
Step-by-Step Implementation
Step 1 — Map Interpreter Boundaries (ASVS V1.5.1, NIST SI-10)
Construct a context-aware data flow diagram before writing any defensive code. Trace every external input from ingress (API gateway, form, webhook, inter-service call) to execution. Label each hop with trust levels: untrusted, semi-trusted (validated but not authenticated), and trusted (authenticated + schema-validated). Explicitly identify where data transitions from a string representation to executable context: database query parsers, OS shells, template engines, serialization libraries, and LDAP filters.
Group inputs by target interpreter. Each context requires a distinct defensive primitive:
- SQL / RDBMS: prepared statements or ORM parameterized bindings
- NoSQL (MongoDB, Elasticsearch): strict schema validation before query object construction
- OS shell: argument arrays (
execFile,subprocess.run(..., shell=False)) - Template engines: pre-compiled templates, auto-escape, no user-authored template strings
- LDAP: RFC 4515 attribute value escaping
- XML parsers: external entity resolution disabled, DTD processing off
Step 2 — Enforce Parameterized Database Queries (ASVS V5.4.1, PCI-DSS Req 6.2.4)
Prepared statements separate query structure from data at the protocol level. The database driver sends the SQL template to the server, the server compiles it, and then binds the parameters as typed values — the parser never sees user data as syntax.
Node.js / PostgreSQL (pg-promise)
// VULNERABLE: string interpolation allows SQL injection
const unsafeQuery = `SELECT id, email FROM users WHERE username = '${req.body.username}'`;
// HARDENED: parameterized binding — $1 is always treated as a typed literal
const result = await db.oneOrNone(
'SELECT id, email FROM users WHERE username = $1',
[req.body.username]
);
Python / SQLAlchemy Core
from sqlalchemy import text
# VULNERABLE: f-string interpolation
query = f"SELECT * FROM orders WHERE status = '{status}'"
# HARDENED: bound parameter — never interpreted as SQL syntax
with engine.connect() as conn:
result = conn.execute(
text("SELECT * FROM orders WHERE status = :status"),
{"status": status}
)
Java / Hibernate JPA Criteria API
// VULNERABLE: JPQL string concatenation
String unsafeJpql = "SELECT u FROM User u WHERE u.role = '" + inputRole + "'";
// HARDENED: Criteria API generates parameterized statements automatically
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.where(cb.equal(root.get("role"), cb.parameter(String.class, "role")));
TypedQuery<User> query = entityManager.createQuery(cq);
query.setParameter("role", inputRole);
List<User> results = query.getResultList();
For comprehensive NoSQL-specific patterns including MongoDB operator injection and Elasticsearch query DSL hardening, see parameterized queries for SQL and NoSQL injection.
Step 3 — Validate NoSQL Query Objects with Strict Schemas (ASVS V5.4.3, NIST SI-10)
NoSQL drivers often accept plain JavaScript objects as query documents. Without schema validation, an attacker can supply { "$gt": "" } where a string is expected, injecting a MongoDB comparison operator that bypasses equality checks.
import { z } from "zod";
import { User } from "./models";
// Schema enforces primitive types — no operator objects can pass
const LoginSchema = z.object({
username: z.string().min(1).max(64).regex(/^[a-zA-Z0-9_.-]+$/),
password: z.string().min(8).max(128),
});
async function loginHandler(body: unknown) {
// Throws ZodError if body contains operator injection attempt
const { username, password } = LoginSchema.parse(body);
// Only string values reach the query — operator injection impossible
const user = await User.findOne({ username, password: hash(password) });
return user;
}
The z.string() type constraint rejects any non-string value including { "$ne": null }. Apply the same pattern with Joi in Node.js or Pydantic in Python.
Step 4 — Eliminate OS Command Shell Interpolation (ASVS V5.4.4, NIST CM-7)
Never pass user-controlled data to functions that spawn a shell (exec, system, popen with shell=True). Use argument array forms that bypass the shell interpreter entirely.
import subprocess
host = request.args.get("host", "")
# VULNERABLE: shell=True passes the entire string to /bin/sh
subprocess.run(f"ping -c 1 {host}", shell=True) # ping -c 1; rm -rf /
# HARDENED: argument array — the shell is never invoked
subprocess.run(["/bin/ping", "-c", "1", host], shell=False, timeout=5)
Validate host against an allowlist of permitted characters (alphanumeric, hyphens, dots) before passing it even as an argument. DNS names do not require semicolons, pipes, or backticks.
Step 5 — Sandbox Template Engine Execution (ASVS V5.5.1, SOC 2 CC6.1)
Server-Side Template Injection occurs when user-controlled input is rendered through a template engine that interprets expression syntax. The safest control is never accepting user-authored template strings — use a pre-compiled, developer-authored template registry and pass user data only as render context variables.
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
# VULNERABLE: user-supplied template string rendered directly
env = Environment(autoescape=False)
env.globals.update({"exec": exec, "eval": eval})
template = env.from_string(request.form["template"]) # RCE risk
template.render()
# HARDENED: templates loaded from trusted filesystem path only
env = Environment(
loader=FileSystemLoader("/app/templates"), # no user-controlled paths
autoescape=select_autoescape(["html", "xml", "j2"]),
undefined=StrictUndefined,
extensions=[] # no extension execution
)
# Remove dangerous built-ins from the global namespace
env.globals.pop("range", None)
env.globals.pop("namespace", None)
# Register only explicitly approved, read-only filters
env.filters["format_date"] = lambda dt: dt.strftime("%Y-%m-%d")
# User data is context only — never template syntax
template = env.get_template("report.html.j2")
output = template.render(username=escape(request.form["username"]))
Edge Cases & Bypass Patterns
Second-Order SQL Injection
Data that passes safe storage (parameterized INSERT) is later retrieved and concatenated into a different query. The storage path is safe; the retrieval path is not. Fix: parameterize every query regardless of whether the data appears “already stored safely.”
ORM Raw Query Bypass
ORMs only protect generated queries. entityManager.createNativeQuery(jpql + userInput), SQLAlchemy’s text() without bound parameters, and Django’s .extra(where=[rawSql]) bypass ORM protections entirely. Run grep -r "createNativeQuery\|raw(\|\.extra(" src/ in CI to detect these patterns.
NoSQL $where JavaScript Operator
MongoDB’s $where clause accepts a JavaScript function string evaluated server-side — a direct OS command injection vector. Disable $where at the database level (security.javascriptEnabled: false in mongod.conf) and block it in your query allowlist.
Jinja2 Object-Traversal SSTI Bypass
Even with auto-escape enabled, an attacker who can influence the template string (not just context variables) can traverse Python’s object model — for example via cycler.__init__.__globals__.os evaluated inside a Jinja expression — to reach os.system. The only safe fix is never calling env.from_string(userInput). Auto-escape is insufficient when the attacker controls template structure rather than render context.
LDAP Injection via DN Components
RFC 4515 special characters (*, (, ), \, \x00) in attribute values must be escaped as \xx hex sequences. Libraries like ldap-escape (Node.js) or ldap3’s escape_filter_chars (Python) handle this; rolling your own is error-prone. Additionally, validate that Distinguished Name components (CN, OU, DC) match strict allowlists before constructing DN strings.
Automated Testing & CI Validation
Unit Test: SQL Injection Rejection
import { describe, it, expect } from "vitest";
import { loginUser } from "./auth";
describe("SQL injection prevention", () => {
it("rejects single-quote injection in username", async () => {
const result = await loginUser("admin'--", "password");
expect(result).toBeNull();
});
it("rejects union-based injection payload", async () => {
const result = await loginUser("' UNION SELECT null,null--", "x");
expect(result).toBeNull();
});
it("rejects NoSQL operator injection in password field", async () => {
await expect(
loginUser("admin", { $gt: "" } as unknown as string)
).rejects.toThrow();
});
});
CI/CD Gate: SAST + DAST
# .github/workflows/injection-gate.yml
name: Injection Security Gate
on: [push, pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Semgrep — injection patterns
uses: semgrep/semgrep-action@v1
with:
config: |
rules:
- id: raw-sql-concatenation
patterns:
- pattern: $DB.query($QUERY + ...)
- pattern: $DB.execute("..." + ...)
message: "Raw SQL concatenation detected — use parameterized queries"
severity: ERROR
languages: [javascript, typescript, python, java]
- id: shell-true-user-input
pattern: subprocess.run(..., shell=True, ...)
message: "Shell=True with potential user input — use argument array"
severity: ERROR
languages: [python]
- id: jinja2-from-string
pattern: env.from_string($USER_INPUT)
message: "User-controlled template string passed to Jinja2"
severity: ERROR
languages: [python]
dast:
runs-on: ubuntu-latest
needs: [sast]
steps:
- name: Start application
run: docker compose up -d app
- name: OWASP ZAP injection scan
uses: zaproxy/action-active-[email protected]
with:
target: "http://localhost:3000"
rules_file_name: ".zap/injection-rules.tsv"
fail_action: true
Compliance Mapping
| Framework | Control ID | Requirement | Satisfied By |
|---|---|---|---|
| OWASP ASVS v4.0 | V5.4.1 | Parameterized statements for all database access | Prepared statements, ORM bindings |
| OWASP ASVS v4.0 | V5.4.3 | Schema validation before NoSQL query construction | Zod / Joi / Pydantic strict parsing |
| OWASP ASVS v4.0 | V5.5.1 | Sandboxed template execution | Jinja2 StrictUndefined, no from_string() |
| OWASP ASVS v4.0 | V5.4.4 | No OS shell interpolation | execFile / shell=False argument arrays |
| SOC 2 | CC6.1 | Logical access and input controls | Input validation at ingress, least-privilege DB accounts |
| SOC 2 | CC7.1 | System monitoring and anomaly detection | SAST gates, DAST runtime probing, WAF anomaly rules |
| PCI-DSS v4.0 | Req 6.2.4 | Prevent injection vulnerabilities in bespoke software | Parameterized queries, SAST on every build |
| PCI-DSS v4.0 | Req 6.3.2 | Inventory of bespoke and custom software | ORM / driver dependency scan in CI |
| NIST SP 800-53 Rev. 5 | SI-10 | Information input validation | Allowlist validation at API boundary |
| NIST SP 800-53 Rev. 5 | CM-7 | Least functionality | Shell=False, disabled template built-ins |
| ISO 27001 | A.14.2.5 | Secure system engineering principles | Secure coding standard enforced via SAST gates |
Common Pitfalls Checklist
Frequently Asked Questions
How does injection prevention differ from XSS mitigation?
Injection targets backend interpreters — databases, OS shells, template engines, LDAP — while XSS mitigation targets client-side browsers and the DOM. Injection prevention requires parameterized query execution and execution context isolation, not HTML entity encoding or Content Security Policy headers. Both require input validation at the boundary, but the execution surface and remediation architecture are entirely different: XSS is a rendering problem, injection is a parser problem.
Are parameterized queries sufficient for all injection types?
No. Parameterization effectively mitigates SQL and NoSQL injection but does not protect against OS command injection, LDAP injection, XXE, or SSTI. Each interpreter requires a context-specific primitive: shell argument arrays (execFile, shell=False) for OS commands; RFC 4515 escaping for LDAP attribute values; DTD-disabled XML parsers for XXE; and pre-compiled, sandboxed templates for SSTI. Applying SQL parameterization and assuming the rest is covered is one of the most common misconfigurations.
When is a WAF an acceptable primary control for injection?
Never. WAFs provide signature-based blocking that fails against encoded payloads (URL encoding, base64, Unicode normalization), out-of-band injection (DNS, time-based blind), and zero-day injection variants. A WAF is a valid compensating control while a code fix is in progress, and a useful defense-in-depth layer after the code is fixed. It is not a substitute for parameterized execution.
How do we validate injection controls in CI/CD pipelines?
Integrate a SAST tool (Semgrep, CodeQL, or SonarQube) with rules targeting raw string concatenation in query paths, shell=True, and env.from_string(). Configure the gate to fail builds on Critical and High severity findings. In the staging environment, run DAST with authenticated scans using OWASP ZAP or Burp Suite in CI mode. Require explicit security review for any PR that touches database access layers, template configuration, or external process spawning. For attack surface discovery tooling, see automated attack surface discovery with OWASP ZAP.
Related
- Parameterized Queries for SQL and NoSQL Injection — driver-level configuration, NoSQL operator injection, and ORM hardening patterns
- Cross-Site Scripting (XSS) Mitigation — escaping, CSP, and DOM sanitization for client-side injection vectors
- Cross-Site Request Forgery (CSRF) Defense — session integrity controls complementing injection prevention
- DOM-Based Vulnerability Sanitization — client-side sanitization patterns for DOM sink injection
- Vulnerability Patterns & Web Mitigation Strategies — parent section covering the full web vulnerability taxonomy