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.

Injection Mitigation Architecture Data flow diagram showing how untrusted user input is isolated from interpreter syntax through input validation and parameterized binding layers, contrasted with the vulnerable direct concatenation path. Untrusted User Input VULNERABLE PATH String Concatenation "SELECT * WHERE id='" + input Interpreter Parses data AS syntax Injection Arbitrary execution SAFE PATH Input Validation Type · Length · Allowlist Parameterized Binding Layer Interpreter Data ≠ Syntax Binding layer ensures the interpreter always treats user data as an opaque literal value, 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