OWASP Injection Prevention
Eliminate SQL, NoSQL, and command injection by never concatenating untrusted input into queries or commands
When to Use
- Writing database queries that accept user-supplied parameters
- Building CLI wrappers or any code that spawns child processes
- Reviewing code that uses string concatenation to build queries
- Implementing search, filter, or dynamic query features
- Working with ORMs, raw query builders, or MongoDB operators
Instructions
SQL Injection
Never build queries with string concatenation. Always use parameterized queries or a query builder.
// BAD — direct interpolation
const query = `SELECT * FROM users WHERE id = ${userId}`;
// GOOD — parameterized (node-postgres)
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD — TypeORM query builder
const user = await userRepo
.createQueryBuilder('user')
.where('user.id = :id', { id: userId })
.getOne();
// GOOD — Prisma (always parameterized)
const user = await prisma.user.findUnique({ where: { id: userId } });
For raw SQL that must be dynamic (table/column names), use an allowlist — never accept these from user input:
const ALLOWED_SORT_COLUMNS = ['name', 'created_at', 'email'] as const;
type SortColumn = (typeof ALLOWED_SORT_COLUMNS)[number];
function buildSortedQuery(column: SortColumn) {
// column is validated at the type level AND runtime
if (!ALLOWED_SORT_COLUMNS.includes(column)) throw new Error('Invalid column');
return `SELECT * FROM users ORDER BY ${column}`;
}
NoSQL Injection (MongoDB)
MongoDB operators like $where, $gt, $regex can be injected through JSON body parsing. Validate and sanitize all operator keys.
// BAD — passes user object directly to find
app.post('/login', async (req, res) => {
const user = await User.findOne({ username: req.body.username, password: req.body.password });
// Attacker sends: { "username": "admin", "password": { "$gt": "" } }
});
// GOOD — extract and validate specific string fields
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '');
const password = String(req.body.password ?? '');
const user = await User.findOne({ username, password });
});
// GOOD — use express-mongo-sanitize middleware to strip $ keys
import mongoSanitize from 'express-mongo-sanitize';
app.use(mongoSanitize());
Command Injection
Never pass user input to exec, execSync, or shell: true spawns.
import { execFile, spawn } from 'child_process';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
// BAD
exec(`convert ${userFilename} output.png`);
// GOOD — execFile does NOT invoke a shell; args are passed directly
await execFileAsync('convert', [userFilename, 'output.png']);
// GOOD — spawn with shell: false (default)
const proc = spawn('convert', [userFilename, 'output.png']);
Input Validation with Zod
Validate all external input at system boundaries before it reaches any query or command:
import { z } from 'zod';
const SearchSchema = z.object({
query: z
.string()
.max(100)
.regex(/^[\w\s]+$/),
page: z.number().int().min(1).max(1000),
sortBy: z.enum(['name', 'date', 'score']),
});
app.get('/search', async (req, res) => {
const params = SearchSchema.parse(req.query); // throws ZodError on invalid input
const results = await db.search(params.query, params.page, params.sortBy);
res.json(results);
});
Details
OWASP consistently ranks injection as a top-3 vulnerability. The root cause is always the same: user-controlled data is interpreted as code or a query operator rather than data.
Defense layers:
- Parameterized queries / prepared statements (primary)
- Input validation with allowlists (secondary)
- ORM/query builder abstractions (structural)
- Least-privilege database accounts (defense-in-depth)
Common bypasses to watch for:
- Second-order injection: input is stored then later interpolated in a different query
- Encoding tricks:
%27→',--URL-encoded as%2D%2D - JSON body parsing creating unexpected object structures
ORMs do NOT automatically protect you if you use raw query escape hatches (Prisma.$queryRaw without template literals, TypeORM query(), etc.).
// STILL vulnerable with Prisma if you concatenate
const bad = await prisma.$queryRaw(`SELECT * FROM users WHERE id = ${id}`);
// SAFE — template literal version uses parameterization automatically
const good = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${id}`;
Source
https://owasp.org/www-project-top-ten/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document consumed as context, but its concrete anti-patterns are also load-bearing: they are enforced mechanically by
harness-security-scanvia the injection rule set in@harness-engineering/core(SEC-INJ-001eval/Function,SEC-INJ-002SQL string concatenation,SEC-INJ-003command injection,SEC-INJ-004Prisma$queryRawUnsafe/$executeRawUnsafewith interpolated input). A finding from any of these rules is a real, citable violation of this skill. - What remains advisory (not mechanized): NoSQL/MongoDB operator injection (passing an unvalidated request body to
find/findOne) and second-order injection both require framework-aware data-flow analysis to detect without a high false-positive rate. Those stay prose guidance here and are covered by/harness:security-review(semantic taint tracking), not the mechanical scan. - No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.