——
GuideHARDENING

Data Injection Prevention: controls that keep data out of the instruction stream

Prevent injection with parameterised APIs, typed allowlists, shell-free design, contextual encoding, least privilege and deployment-level regression tests.

Prevent injection with parameterised APIs, typed allowlists, shell-free design, contextual encoding, least privilege and deployment-level regression tests.

Data injectionSecure codingParameterised queriesInput validationOWASP A05

Was I hacked?

Prevention work and incident response are different workstreams. If telemetry shows suspicious requests followed by abnormal queries, process creation, files, accounts or exports, preserve evidence and contain the system before changing it. A hurried code deployment can erase useful state while leaving stolen credentials valid.

If you have found vulnerable construction but no exploitation evidence, record the affected versions and exposure window, add temporary restrictions, repair the boundary and investigate historical telemetry. “We parameterised it today” does not answer what happened last week.

The prevention model

Reliable injection prevention follows this order:

  1. remove the interpreter when possible;
  2. use a structured API that separates instructions from values;
  3. allowlist the small pieces of structure that must vary;
  4. encode for the exact destination context when separation is unavailable;
  5. validate business type, size and range on the server;
  6. run the component with least privilege;
  7. prove the boundary with automated tests and deployed validation;
  8. log rejection decisions without logging dangerous or sensitive content.

The ordering matters. Input validation improves correctness, but a regular expression is not a replacement for SQL parameters or an argument array.

Control 1: remove the interpreter

The best shell-injection control is often not invoking a shell. Use a language library for filesystem, DNS, archive, image and HTTP operations.

Unsafe:

exec(`mkdir -p ${directory}`, callback);

Preferred:

import { mkdir } from 'node:fs/promises';

await mkdir(directory, {
  recursive: true,
  mode: 0o750
});

The filesystem API performs one operation. It cannot unexpectedly become a pipeline, redirection or second command.

Likewise, use a fixed template file rather than compiling a user-provided template string, and a database driver rather than constructing a database command in a shell.

Control 2: parameterise SQL values

Node.js with PostgreSQL:

const result = await pool.query(
  `SELECT id, total, status
     FROM invoices
    WHERE tenant_id = $1
      AND id = $2`,
  [req.user.tenantId, req.params.invoiceId]
);

Python DB-API:

cursor.execute(
    "SELECT id, total, status FROM invoices WHERE tenant_id = %s AND id = %s",
    (tenant_id, invoice_id),
)

Java JDBC:

PreparedStatement statement = connection.prepareStatement(
    "SELECT id, total, status FROM invoices WHERE tenant_id = ? AND id = ?"
);
statement.setString(1, tenantId);
statement.setString(2, invoiceId);
ResultSet results = statement.executeQuery();

Use the placeholder syntax documented by the actual driver. Do not quote the placeholder as if it were a string literal. The OWASP Query Parameterization Cheat Sheet contains platform-specific patterns.

Parameterisation does not provide authorisation. The tenant predicate in the examples is still required.

Control 3: map dynamic identifiers

Table names, column names and SQL keywords commonly cannot be bound as values. Redesign where possible. Otherwise map a finite user choice to complete code-owned fragments:

const SORT_SQL = Object.freeze({
  newest: 'created_at DESC',
  oldest: 'created_at ASC',
  amount_high: 'total DESC'
});

const orderBy = SORT_SQL[req.query.sort] ?? SORT_SQL.newest;
const result = await pool.query(
  `SELECT id, total FROM invoices
    WHERE tenant_id = $1
    ORDER BY ${orderBy}
    LIMIT $2`,
  [req.user.tenantId, 50]
);

Do not accept a column name and remove punctuation until it “looks safe”. The server should own the vocabulary.

Control 4: build NoSQL queries on the server

Do not pass a request body directly to a query method:

// Unsafe
await users.findOne(req.body);

Validate types and construct the filter:

const email = String(req.body.email ?? '').trim().toLowerCase();
if (email.length < 3 || email.length > 254 || !email.includes('@')) {
  return res.status(400).json({ error: 'Invalid email' });
}

await users.findOne({
  email,
  tenantId: req.user.tenantId,
  disabled: false
});

At the HTTP boundary, use JSON Schema with additionalProperties: false, explicit primitive types, maximum lengths and enumerations. If an endpoint genuinely offers filters, translate a documented filter language into server-owned query objects; never expose the database’s native expression language directly.

Control 5: avoid shells and control arguments

When an external executable is unavoidable:

import { execFile } from 'node:child_process';

const allowedTypes = new Set(['A', 'AAAA', 'MX']);
const recordType = allowedTypes.has(req.query.type) ? req.query.type : 'A';

execFile('/usr/bin/dig', ['+short', recordType, '--', hostname], {
  shell: false,
  timeout: 5000,
  maxBuffer: 64 * 1024,
  env: { PATH: '/usr/bin:/bin', LANG: 'C' }
}, callback);

Confirm that the called utility supports -- in that position; interfaces differ. The executable path is fixed, the operation is allowlisted, arguments are separate, resources are bounded and the environment is minimal.

The service identity should not be able to modify application code, read deployment keys or reach arbitrary networks.

Control 6: encode for the exact context

Some interpreters do not offer convenient parameterisation. Use a maintained encoder for the precise grammar and position.

LDAP search filter:

String filter = "(&(uid={0})(objectClass=person))";
ctx.search(baseDn, filter, new Object[] { userInput }, controls);

An LDAP distinguished name needs different encoding from an LDAP filter. HTML text, HTML attributes, JavaScript strings and URLs likewise have different output-encoding rules. A method named escape() without a destination in its contract is technical debt wearing a reassuring badge.

Control 7: validate the business value

OWASP recommends server-side allowlist validation for structured values. Validate:

  • expected primitive type;
  • minimum and maximum length;
  • numerical or date range;
  • complete allowed character or Unicode categories where appropriate;
  • exact enumeration for finite choices;
  • object shape and rejection of unknown properties;
  • canonical representation before comparison.

Example with a finite report format:

const formats = new Set(['pdf', 'csv']);
if (!formats.has(req.body.format)) {
  return res.status(400).json({ error: 'Unsupported format' });
}

Do not ban apostrophes from names to make an unsafe SQL query quiet. That harms users and preserves the flaw.

Control 8: restrict privileges and egress

Use separate identities for reading, writing, migrations, backups and administration. An application connection should not own the schema merely because deployment was easier on Friday afternoon.

For each service, document:

database tables and operations required
filesystem paths required
child processes permitted
outbound destinations required
secrets readable
administrative interfaces reachable

Enforce the result with database grants, service managers, containers, AppArmor or SELinux, network policy and secret-manager access controls. Test restrictions; configuration prose does not deny a packet.

Control 9: protect logs and exports

Emit structured events through a logging library. Remove carriage returns and line feeds from bounded display values, and never construct JSON manually:

const safeLabel = String(label ?? '')
  .normalize('NFKC')
  .replace(/[\r\n\u2028\u2029]/g, ' ')
  .slice(0, 80);

logger.warn({
  event: 'input_validation_failure',
  field: 'label',
  valueLength: String(label ?? '').length,
  safeLabel,
  requestId: req.id
});

Exclude passwords, tokens, cookies and sensitive bodies. For CSV or spreadsheet export, apply a documented formula policy and test the actual supported clients.

Control 10: make the fix testable

Unit tests should assert the interpreter call:

expect(pool.query).toHaveBeenCalledWith(
  'SELECT id FROM users WHERE email = $1',
  ["o'brien@example.test"]
);

Integration tests should exercise valid, malformed and syntax-shaped values in an isolated environment. Confirm:

  • normal business data still works;
  • invalid types fail before the sink;
  • the result set does not widen;
  • no interpreter error reaches the client;
  • no unexpected process, file or network event occurs;
  • the rejection event is structured and contains no secret.

After deployment, repeat a safe regression case against the released version. A green local branch cannot protect a production process it never reached.

Legacy software and virtual patching

When source code cannot be fixed immediately:

  1. remove public exposure where possible;
  2. restrict routes and identities at a reverse proxy or gateway;
  3. deploy vendor-supported WAF rules in block mode after testing;
  4. reduce runtime privileges and outbound access;
  5. increase application, database and endpoint monitoring;
  6. set a dated owner for replacement or vendor remediation.

A WAF is a temporary compensating control. It cannot see every queue, internal call, alternate encoding or stored value. Record the residual risk and expiry date.

Pull-request checklist

  • No untrusted value is concatenated into interpreter structure.
  • Dynamic structure comes from a server-side mapping.
  • Values have explicit server-side types and bounds.
  • The runtime identity has minimum permissions.
  • Tests assert the downstream call and deployed result.
  • Rejection events are structured and privacy-aware.
  • Historical exposure was reviewed for incident evidence.
  • The original unsafe path is removed, not merely wrapped and forgotten.

See Data Injection Examples for more language-specific patterns and How to Detect Data Injection for the monitoring workflow.

References

Reviewed 5 September 2026.