4 min read
On this page

Other Injection Types

SQL injection and XSS get the most attention, but injection vulnerabilities exist wherever user input reaches an interpreter. Command injection, path traversal, SSRF, template injection, and NoSQL injection all follow the same principle: the application trusts user input and passes it to a system that interprets it as instructions. The defense is always the same — never trust user input, no matter the source.

Command Injection

Command injection occurs when user input is passed to a system shell. The attacker breaks out of the intended command and executes arbitrary commands on the server.

// Vulnerable: user input passed to a shell command
// The application lets users ping a host for diagnostics
const { exec } = require('child_process');
exec('ping -c 4 ' + userInput);

// Normal input: 8.8.8.8
// Command: ping -c 4 8.8.8.8

// Malicious input: 8.8.8.8; cat /etc/passwd
// Command: ping -c 4 8.8.8.8; cat /etc/passwd
// The attacker reads the system's password file

// Worse input: 8.8.8.8; rm -rf /
// The attacker deletes the entire filesystem

Shell Metacharacters

Attackers use shell metacharacters to chain or modify commands: ; (command separator), && and || (logical operators), | (pipe), $() and backticks (command substitution), > (redirect). Any of these can be used to append arbitrary commands to the intended one.

The Fix: Never Use Shell Commands with User Input

// Option 1: Use language-native libraries instead of shell commands
// Instead of: exec('ping ' + host)
// Use a library that makes ICMP requests directly

// Option 2: If you must call external programs, avoid the shell
// Node.js — use execFile, not exec
const { execFile } = require('child_process');
execFile('ping', ['-c', '4', userInput]);
// execFile does not invoke a shell, so metacharacters are not interpreted

// Python — use subprocess with shell=False (the default)
import subprocess
subprocess.run(['ping', '-c', '4', user_input])
// Arguments are passed directly, not through a shell

// Option 3: Validate input strictly
// If the input should be an IP address, validate it is an IP address
const ipRegex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
if (!ipRegex.test(userInput)) {
  throw new Error('Invalid IP address');
}

Real-World Command Injection

The 2014 Shellshock vulnerability (CVE-2014-6271) allowed command injection through Bash environment variables. CGI web servers passed HTTP headers as environment variables, letting attackers inject commands through User-Agent headers. It affected millions of servers and was exploited within hours of disclosure. In 2021, GitLab suffered command injection (CVE-2021-22205) through ExifTool image processing — attackers uploaded crafted images to execute arbitrary commands on the server.

Path Traversal

Path traversal (directory traversal) occurs when user input controls a file path, allowing attackers to read or write files outside the intended directory.

// Vulnerable: user input in a file path
// Application serves user-uploaded files
app.get('/files', (req, res) => {
  const filename = req.query.name;
  res.sendFile('/uploads/' + filename);
});

// Normal request: /files?name=report.pdf
// Serves: /uploads/report.pdf

// Malicious request: /files?name=../../../etc/passwd
// Serves: /uploads/../../../etc/passwd -> /etc/passwd
// The attacker reads system files

// Malicious request: /files?name=....//....//....//etc/shadow
// Double-encoding and alternate traversal sequences bypass naive filters

The Fix: Validate & Constrain File Paths

// Option 1: Resolve the path and verify it is within the allowed directory
const path = require('path');
const basedir = '/uploads';
const resolved = path.resolve(basedir, userInput);
if (!resolved.startsWith(basedir + '/')) {
  throw new Error('Path traversal detected');
}

// Option 2: Use an allowlist of valid filenames
const allowedFiles = new Set(['report.pdf', 'summary.csv', 'data.json']);
if (!allowedFiles.has(userInput)) {
  throw new Error('File not found');
}

// Option 3: Use an ID-based lookup instead of filenames
// Instead of: /files?name=report.pdf
// Use: /files?id=abc123
// Map the ID to the filename on the server side

Path Traversal Bypass Techniques

Attackers bypass naive ../ stripping with URL encoding (..%2f), double encoding (..%252f), nested traversal (....//), backslashes on Windows (..%5c), and null bytes in older frameworks. Simple blocklist filtering is never sufficient.

Server-Side Request Forgery (SSRF)

SSRF occurs when an application makes HTTP requests to URLs controlled by the attacker. The attacker uses the server as a proxy to access internal resources that are not directly accessible.

// Vulnerable: application fetches a URL provided by the user
// "Enter a URL to generate a preview"
app.post('/preview', async (req, res) => {
  const response = await fetch(req.body.url);
  const html = await response.text();
  res.send(generatePreview(html));
});

// Normal input: https://example.com/article
// Server fetches the public article and generates a preview

// Malicious input: http://169.254.169.254/latest/meta-data/iam/security-credentials/
// Server fetches AWS instance metadata, returning IAM credentials
// The attacker now has cloud credentials

// Malicious input: http://localhost:6379/
// Server connects to the internal Redis instance
// The attacker can send commands to internal services

// Malicious input: http://internal-admin.corp:8080/admin/delete-all
// Server makes a request to an internal admin panel

The 2019 Capital One Breach

2019 — Capital One (CVE-2019-0211 related)
- An SSRF vulnerability in a web application firewall
- The attacker used SSRF to access the AWS metadata endpoint
  (169.254.169.254) and obtain IAM role credentials
- With those credentials, the attacker accessed S3 buckets containing
  106 million customer records including SSNs and bank account numbers
- The attacker was a former AWS employee who knew the metadata endpoint
- Capital One was fined $80 million
- This breach was the catalyst for AWS adding IMDSv2 (requiring a token
  for metadata access instead of simple GET requests)

The Fix: Validate URLs & Block Internal Networks

// Step 1: Parse the URL and validate the scheme
const url = new URL(userInput);
if (!['http:', 'https:'].includes(url.protocol)) {
  throw new Error('Invalid protocol');
}

// Step 2: Resolve the hostname and block internal IPs
// Block: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
// Block: 169.254.169.254 (cloud metadata)
// Block: fd00::/8 (IPv6 private)
const ip = await dns.resolve(url.hostname);
if (isInternalIP(ip)) {
  throw new Error('Internal addresses are not allowed');
}

// Step 3: Use an allowlist when possible
const allowedDomains = ['example.com', 'trusted-partner.com'];
if (!allowedDomains.includes(url.hostname)) {
  throw new Error('Domain not allowed');
}

// Step 4: Use a dedicated egress proxy with network-level restrictions
// Route outbound requests through a proxy that blocks internal networks

Template Injection

Template injection occurs when user input is embedded in a server-side template and the template engine interprets it as a template expression.

// Vulnerable: user input in a Jinja2 template (Python)
template = Template("Hello " + user_input)

// Normal input: Alice
// Output: Hello Alice

// Malicious input: {{7*7}}
// Output: Hello 49  (the template engine evaluated the expression)

// Dangerous input: {{config.items()}}
// Output: all Flask configuration values including SECRET_KEY

// Remote code execution:
// {{''.__class__.__mro__[1].__subclasses__()}}
// Lists all Python classes available, some of which can execute commands

Server-Side Template Injection (SSTI) in the Wild

2016 — Uber
- SSTI vulnerability in a Jinja2 template
- Researcher demonstrated remote code execution
- Earned a $10,000 bug bounty

The fix: never pass user input as part of a template string.
Always pass user input as template data/variables:

// Dangerous
template = Template("Hello " + user_input)

// Safe
template = Template("Hello {{ name }}")
template.render(name=user_input)

NoSQL Injection

NoSQL databases like MongoDB are not immune to injection. Instead of SQL syntax, attackers inject query operators.

// Vulnerable MongoDB query (Node.js)
db.users.find({
  username: req.body.username,
  password: req.body.password
});

// Normal input: { "username": "admin", "password": "secret123" }
// Query: find({ username: "admin", password: "secret123" })

// Malicious input: { "username": "admin", "password": { "$ne": "" } }
// Query: find({ username: "admin", password: { $ne: "" } })
// The $ne (not equal) operator matches any non-empty password
// The attacker logs in as admin without knowing the password

// Other dangerous operators:
// $gt    Greater than (password: { $gt: "" } matches all)
// $regex  Regular expression (password: { $regex: "^a" } tests first char)
// $where  Execute JavaScript (username: { $where: "sleep(5000)" })

The Fix: Validate Input Types

// Validate that inputs are strings, not objects
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') {
  throw new Error('Invalid input type');
}

// Use a schema validation library (Zod, Joi, etc.)
const schema = z.object({
  username: z.string().min(1).max(100),
  password: z.string().min(1).max(200),
});
const validated = schema.parse(req.body);

The Principle: Never Trust User Input

Every injection type follows the same pattern:

1. The application receives input from a user (or any external source)
2. The application passes that input to an interpreter without sanitization
3. The interpreter executes the input as instructions instead of treating it as data

The fix is always one of:
- Parameterize: separate code from data (SQL, LDAP)
- Avoid the interpreter: use APIs instead of shell commands
- Validate strictly: allowlist acceptable values, reject everything else
- Encode output: context-aware encoding for HTML, JS, URLs
- Type-check: ensure input matches the expected type (NoSQL)

User input is not just form fields. It includes URL parameters, HTTP headers (User-Agent, Referer, Cookie), uploaded files, API request bodies, WebSocket messages, data from your own database (second-order injection), data from third-party APIs, and environment variables set by proxies. If you did not generate it yourself in your own code, do not trust it.

Common Pitfalls

  • Only validating form fields. Attackers inject through headers, cookies, file uploads, and API parameters. Validate every input, not just the obvious ones.

  • Blocklist-based filtering. Trying to block dangerous characters or patterns. Attackers have more bypass techniques than you have filters. Use allowlists and parameterization instead.

  • Trusting "internal" APIs. Microservices calling each other still pass data. If service A receives user input and passes it to service B, service B must validate it. Do not assume upstream services sanitized the data.

  • Assuming HTTPS prevents injection. HTTPS encrypts data in transit. It does nothing to prevent injection.

  • Rolling your own sanitization. Writing custom regex to strip dangerous characters is error-prone. Use well-tested libraries: parameterized queries for SQL, DOMPurify for HTML, execFile for commands.

  • Ignoring SSRF in cloud environments. The cloud metadata endpoint (169.254.169.254) is the most valuable SSRF target. It returns credentials and API keys. Block it by default.

Key Takeaways

  • Command injection happens when user input reaches a shell. Never pass user input to exec or system. Use execFile or language-native libraries instead.
  • Path traversal happens when user input controls file paths. Resolve paths and verify they remain within the allowed directory. Use allowlists or ID-based lookups.
  • SSRF happens when user input controls URLs the server fetches. Validate URLs, block internal IP ranges, and block cloud metadata endpoints. The Capital One breach was caused by SSRF.
  • Template injection happens when user input is embedded in template strings. Always pass user input as template variables, never as part of the template itself.
  • NoSQL injection uses query operators instead of SQL syntax. Validate that inputs match expected types and strip operator keys.
  • The universal principle: never trust user input, regardless of its source. Validate input for correctness, parameterize queries, avoid shell interpreters, and encode output.