Data Injection Examples: seven unsafe patterns and their safe replacements
Concrete SQL, NoSQL, command, LDAP, template, log and CSV injection examples, each paired with safer code and a regression check.
Scope
Concrete SQL, NoSQL, command, LDAP, template, log and CSV injection examples, each paired with safer code and a regression check.
Was I hacked?
Unsafe code is evidence of exposure, not automatically evidence of use. If one of the patterns below exists in production, determine when it was introduced, which routes reach it, which identities execute it and what telemetry covers that period.
Treat the system as a likely incident when the vulnerable path aligns with abnormal queries, broad result sets, a web-server child process, altered files, new accounts, unexpected exports or outbound connections. Isolate the affected function without destroying logs, preserve evidence and rotate credentials that the vulnerable process could read.
This article uses deliberately harmless names and documentation domains. Run the regression checks only in code you own or an environment where you have explicit permission.
Example 1: SQL injection through string concatenation
Unsafe
const email = req.query.email;
const sql = `SELECT id, email, role FROM users WHERE email = '${email}'`;
const result = await pool.query(sql);
The application puts the value inside SQL syntax. Escaping individual quote characters is fragile and database-specific.
Safer replacement
const email = String(req.query.email ?? '').trim().toLowerCase();
if (email.length > 254) return res.status(400).send('Invalid email');
const result = await pool.query(
'SELECT id, email, role FROM users WHERE email = $1',
[email]
);
The PostgreSQL driver receives statement structure and values separately. The application’s database role should have only the tables and operations this feature needs.
Regression check
expect(pool.query).toHaveBeenCalledWith(
'SELECT id, email, role FROM users WHERE email = $1',
["o'brien@example.test"]
);
The test asserts construction, not simply a 200 response. OWASP recommends prepared statements as the primary SQL-injection defence.
Example 2: dynamic SQL identifiers
Bind parameters normally represent values, not column names or keywords. This remains unsafe:
const sql = `SELECT id, title FROM reports ORDER BY ${req.query.sort}`;
await pool.query(sql);
Map a small business choice to a complete code-owned fragment:
const sortOptions = Object.freeze({
newest: 'created_at DESC',
oldest: 'created_at ASC',
title: 'title ASC'
});
const orderBy = sortOptions[req.query.sort] ?? sortOptions.newest;
await pool.query(
`SELECT id, title FROM reports ORDER BY ${orderBy} LIMIT $1`,
[50]
);
Test every accepted key and one rejected key:
expect(sortOptions.newest).toBe('created_at DESC');
expect(sortOptions['created_at; something']).toBeUndefined();
Do not build a regular expression that attempts to recognise every dangerous SQL word. Choose the three valid operations and reject everything else.
Example 3: NoSQL operator injection
Parsed JSON can carry structure. Passing a client body directly to a database driver delegates query design to the client:
// Unsafe
const account = await accounts.findOne(req.body);
Construct a new filter from validated primitives and reject additional fields at the request schema:
const accountId = String(req.body.accountId ?? '');
if (!/^[a-f0-9]{24}$/i.test(accountId)) {
return res.status(400).json({ error: 'Invalid account ID' });
}
const account = await accounts.findOne({
_id: new ObjectId(accountId),
tenantId: req.user.tenantId
});
An integration test should submit an object where a string is expected and confirm a 400 response before any database call:
await request(app)
.post('/account/lookup')
.send({ accountId: { unexpected: 'object' } })
.expect(400);
expect(accounts.findOne).not.toHaveBeenCalled();
The OWASP NoSQL Security Cheat Sheet recommends server-constructed query objects and strict control of client-supplied operators.
Example 4: OS command and argument injection
This code invites a shell to interpret a combined string:
# Unsafe
subprocess.run(f"/usr/bin/file {upload_path}", shell=True)
Prefer a language library. If the executable is required, resolve the path beneath a fixed root, pass an argument array, end option processing and impose limits:
from pathlib import Path
import subprocess
root = Path("/srv/app/uploads").resolve()
candidate = (root / upload_name).resolve()
if root not in candidate.parents or not candidate.is_file():
raise ValueError("Invalid upload path")
completed = subprocess.run(
["/usr/bin/file", "--brief", "--", str(candidate)],
shell=False,
check=True,
timeout=5,
capture_output=True,
text=True,
env={"PATH": "/usr/bin:/bin"},
)
Test a filename beginning with a hyphen and a filename containing spaces. Both should remain single operands. Also run the service as an identity that cannot write application code or read unrelated secrets.
OWASP’s command-injection guidance makes an important distinction: preventing shell separators does not automatically prevent hostile command-line options.
Example 5: LDAP filter injection
LDAP distinguished names and search filters have different escaping rules. String concatenation is unsafe:
String filter = "(&(uid=" + userInput + ")(objectClass=person))";
NamingEnumeration<SearchResult> results =
ctx.search("ou=users,dc=example,dc=test", filter, controls);
Use the framework’s parameter substitution:
String filter = "(&(uid={0})(objectClass=person))";
NamingEnumeration<SearchResult> results = ctx.search(
"ou=users,dc=example,dc=test",
filter,
new Object[] { userInput },
controls
);
The directory bind identity should normally have read access only to the attributes required by the application. Test that special LDAP filter characters remain part of the search value and do not broaden the result set.
Use a maintained encoder if the API does not parameterise the relevant position. The OWASP LDAP guidance explains why filter encoding and DN encoding cannot be interchanged.
Example 6: template injection
The dangerous design treats user input as template source:
# Unsafe: user_template controls the template program.
return render_template_string(user_template, customer=customer)
Use a fixed, reviewed template and pass the value as data:
return render_template(
"customer-message.html",
customer_name=customer.display_name,
message=message,
)
Keep automatic HTML escaping enabled and do not mark untrusted values as safe. A regression test can use a harmless expression-shaped string and confirm it renders literally rather than being evaluated:
response = client.post("/preview", json={"message": "{{ 7 * 7 }}"})
assert "{{ 7 * 7 }}" in response.text
assert ">49<" not in response.text
This is a safe local test of the data boundary; it is not an instruction to probe somebody else’s service.
Example 7: log injection
Logs are another interpreter boundary. An untrusted value containing carriage returns, line feeds or delimiters can forge records in line-oriented text.
Avoid assembling log lines:
// Unsafe
logger.info(`login failed user=${req.body.username}`);
Emit a structured event and record a bounded, normalised identifier rather than an entire hostile payload:
const username = String(req.body.username ?? '')
.normalize('NFKC')
.replace(/[\r\n\u2028\u2029]/g, ' ')
.slice(0, 80);
logger.warn({
event: 'authentication_failure',
username,
requestId: req.id,
sourceIp: req.ip,
outcome: 'denied'
});
The log transport and storage layer must encode JSON correctly. Never hand-build JSON with concatenation. Test that a newline remains inside one structured event and that secrets, cookies and authorisation headers are excluded.
OWASP’s Logging Cheat Sheet recommends validating event data from other trust zones, sanitising carriage returns and line feeds and protecting log integrity.
Bonus example: CSV formula injection
Spreadsheet applications may interpret cells beginning with formula-capable characters. An export must apply a documented policy at the spreadsheet boundary.
function spreadsheetCell(value) {
const text = String(value ?? '').replace(/[\r\n]+/g, ' ');
return /^[=+\-@]/.test(text) ? `'${text}` : text;
}
The exact neutralisation policy depends on the target spreadsheet format and organisation. Prefer a library that writes a real spreadsheet format, document how formulas are handled and test the exported file in the supported spreadsheet clients. Do not assume CSV is “just text” after a spreadsheet opens it.
How to review an example in your own code
For each sink, write down:
source: request, queue, file, database or administrator field
transformations: decoding, normalisation, storage and retrieval
sink: exact method and interpreter
separation: parameter, argument array, fixed template or contextual encoder
runtime identity: database role, OS user, directory bind account
test: valid, malformed and syntax-shaped values
telemetry: request ID, validation event, downstream result and side effect
This makes code review repeatable. “Looks sanitised” is not a test result.
What not to copy
- Do not copy payload lists into production and call that a test plan.
- Do not run active scanners against systems you do not own or administer with explicit permission.
- Do not log complete request bodies in the hope of making detection easier.
- Do not use a WAF alert as proof that the application is safe or compromised.
- Do not replace parameterisation with a home-grown escaping function.
For the full implementation sequence, read Data Injection Prevention. For evidence and tooling, continue with How to Detect Data Injection.
References
- OWASP Injection Prevention Cheat Sheet
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP NoSQL Security Cheat Sheet
- OWASP OS Command Injection Defense Cheat Sheet
- OWASP LDAP Injection Prevention Cheat Sheet
- OWASP Logging Cheat Sheet
- MITRE CWE-74: Injection
Reviewed 5 September 2026.