Imported from HermeticOrmus/LibreSecOps-Claude-Code (
plugins/secure-coding-practices/skills/injection-prevention/SKILL.md). Install upstream withnpx skills add HermeticOrmus/LibreSecOps-Claude-Code --skill injection-prevention. Copyright stays with the author.
Injection Prevention
Comprehensive injection prevention patterns for SQL, XSS, command injection, path traversal, LDAP, and template injection across all major languages and frameworks.
Knowledge Base
The Fundamental Principle
Injection occurs when untrusted data is interpreted as code or structure by a downstream processor. The universal prevention is to ensure the processor can distinguish data from code. This is achieved through:
- Parameterization -- Sending data through a separate channel from code (prepared statements, argument arrays)
- Context-aware encoding -- Transforming data so it is inert in the target context (HTML encoding, URL encoding)
- Allowlist validation -- Accepting only data that matches a known-safe pattern (enum values, regex matching)
Denylist filtering (blocking known-bad characters) is almost always insufficient because there are too many encoding variations and bypass techniques.
SQL Injection
Root cause: User input concatenated into SQL query strings.
Prevention by language:
# VULNERABLE
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# SECURE - Parameterized query
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
# SECURE - Django ORM (parameterized by default)
User.objects.filter(name=name)
# VULNERABLE - Django raw with string formatting
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")
# SECURE - Django raw with params
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])
// VULNERABLE
db.query(`SELECT * FROM users WHERE name = '${name}'`);
// SECURE - Parameterized (pg library)
db.query('SELECT * FROM users WHERE name = $1', [name]);
// SECURE - Prisma ORM (parameterized by default)
prisma.user.findMany({ where: { name } });
// VULNERABLE - Sequelize literal
sequelize.query(`SELECT * FROM users WHERE name = '${name}'`);
// SECURE - Sequelize parameterized
sequelize.query('SELECT * FROM users WHERE name = :name', {
replacements: { name }
});
// VULNERABLE
Statement stmt = conn.createStatement();
stmt.execute("SELECT * FROM users WHERE name = '" + name + "'");
// SECURE - PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE name = ?"
);
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
// SECURE - JPA/Hibernate named parameters
Query q = em.createQuery(
"SELECT u FROM User u WHERE u.name = :name"
);
q.setParameter("name", name);
// VULNERABLE
db.Query("SELECT * FROM users WHERE name = '" + name + "'")
// SECURE - Parameterized
db.Query("SELECT * FROM users WHERE name = $1", name)
// SECURE - GORM (parameterized by default)
db.Where("name = ?", name).Find(&users)
Tricky cases:
- ORDER BY: Cannot be parameterized in most databases. Use allowlist validation:
if column not in ALLOWED_COLUMNS: raise Error - LIKE clauses: Parameterize the value but escape
%and_within user input:WHERE name LIKE '%' || $1 || '%'with%and_escaped in the parameter - IN clauses: Generate the right number of placeholders:
WHERE id IN ($1, $2, $3)with one param per value - Table/column names: Cannot be parameterized. Strict allowlist validation required.
- Second-order injection: Data stored safely in the database but later retrieved and used in an unsafe query. The fix is to parameterize ALL queries, not just those with direct user input.
Cross-Site Scripting (XSS)
Root cause: Untrusted data rendered in HTML without proper context-aware encoding.
Context-specific encoding:
| Context | Encoding | Example |
|---|---|---|
| HTML body | HTML entity encoding | < -> < |
| HTML attribute | HTML attribute encoding | " -> " |
| JavaScript | JavaScript encoding | ' -> \x27 |
| URL parameter | URL encoding | -> %20 |
| CSS | CSS encoding | ( -> \28 |
Framework auto-escaping (know what your framework does and does not escape):
| Framework | Auto-escapes | Bypass syntax (dangerous) |
|---|---|---|
| React JSX | Yes (HTML context) | dangerouslySetInnerHTML |
| Angular | Yes (HTML context) | bypassSecurityTrustHtml() |
| Vue | Yes ({{ }}) |
v-html directive |
| Django templates | Yes ({{ }}) |
` |
| Jinja2 | Yes (if configured) | ` |
| Rails ERB | No (<%= %>) / Yes (<%== %>) |
raw(), html_safe |
| Go html/template | Yes | Using text/template instead |
DOM-based XSS: The server never sees the payload. It exists entirely in client-side JavaScript.
// VULNERABLE - DOM XSS
document.getElementById('output').innerHTML = location.hash.substring(1);
// SECURE - Use textContent for text
document.getElementById('output').textContent = location.hash.substring(1);
// SECURE - Use DOMPurify for HTML that must be rendered
import DOMPurify from 'dompurify';
document.getElementById('output').innerHTML = DOMPurify.sanitize(userHtml);
Content Security Policy (defense-in-depth):
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';
Command Injection
Root cause: User input passed to shell interpreters.
Prevention: Use argument arrays, never shell strings.
# VULNERABLE
os.system(f"convert {filename} output.png")
subprocess.call(f"convert {filename} output.png", shell=True)
# SECURE - Argument array, no shell
subprocess.run(["convert", filename, "output.png"], shell=False)
// VULNERABLE
exec(`convert ${filename} output.png`);
// SECURE - execFile with argument array
execFile("convert", [filename, "output.png"]);
// VULNERABLE
exec.Command("sh", "-c", "convert " + filename + " output.png")
// SECURE - Direct command with argument array
exec.Command("convert", filename, "output.png")
Additional safeguards: Validate filename against allowlist pattern ([a-zA-Z0-9._-]+), use full path for command (/usr/bin/convert), drop privileges if possible.
Path Traversal
Root cause: User input used to construct file paths without canonicalization and boundary checking.
# VULNERABLE
filepath = os.path.join(UPLOAD_DIR, user_filename)
with open(filepath) as f: return f.read()
# SECURE - Canonical path validation
filepath = os.path.join(UPLOAD_DIR, user_filename)
canonical = os.path.realpath(filepath)
if not canonical.startswith(os.path.realpath(UPLOAD_DIR)):
raise SecurityError("Path traversal attempt")
with open(canonical) as f: return f.read()
// SECURE - filepath.Clean + prefix check
cleaned := filepath.Clean(filepath.Join(uploadDir, userFilename))
if !strings.HasPrefix(cleaned, filepath.Clean(uploadDir)) {
return errors.New("path traversal attempt")
}
Bypass awareness: ../, ..%2f, ..%252f (double encoding), ....// (filter bypass), null bytes (%00 in older systems), symlink following.
Template Injection (SSTI)
Root cause: User input inserted into template code rather than template data.
# VULNERABLE - User input in template string
template = Template(f"Hello {user_input}")
template.render()
# SECURE - User input as template variable
template = Template("Hello {{ name }}")
template.render(name=user_input)
If users must provide template-like input, use a sandboxed template engine (Jinja2 SandboxedEnvironment) and validate against dangerous constructs.
Patterns
Pattern: Input Validation Layer
Create a validation layer at the application boundary that rejects invalid input before it reaches business logic. Use schema validation libraries (Joi, Zod, Pydantic, marshmallow) with strict type definitions.
Pattern: Output Encoding Layer
Centralize output encoding in template helpers or middleware rather than relying on individual developers to encode at each output point. Choose frameworks with auto-escaping enabled by default.
Pattern: Parameterized Everywhere
Use parameterized interfaces for all structured data contexts (SQL, LDAP, XPath), not just the ones you think receive user input. Data flows change; parameterized queries are safe regardless of data source.
Anti-Patterns
- Denylist filtering: Blocking
<script>does not prevent<img onerror=>,<svg onload=>, or hundreds of other XSS vectors - Escaping for SQL instead of parameterizing: Manual escaping is error-prone and database-specific. Parameterized queries are universally correct
- Client-side-only validation: Any validation in JavaScript can be bypassed by sending requests directly. Server-side validation is mandatory
- WAF as the sole defense: WAFs are bypassable. They are a useful layer but not a substitute for secure code
- Double encoding to "fix" display issues: If output encoding breaks your display, you are encoding at the wrong layer, not "too much"
- Stripping instead of rejecting: Silently removing dangerous characters changes user input in unexpected ways and may be bypassed. Reject invalid input with a clear error
References
- OWASP Injection Prevention Cheat Sheet -- https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html
- OWASP XSS Prevention Cheat Sheet -- https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP SQL Injection Prevention -- https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- OWASP OS Command Injection Defense -- https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
- OWASP Input Validation Cheat Sheet -- https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- CWE-89: SQL Injection -- https://cwe.mitre.org/data/definitions/89.html
- CWE-79: Cross-site Scripting -- https://cwe.mitre.org/data/definitions/79.html
- CWE-78: OS Command Injection -- https://cwe.mitre.org/data/definitions/78.html
- CWE-22: Path Traversal -- https://cwe.mitre.org/data/definitions/22.html