Lesson 12 / 28
Command Injection and Path Traversal
Never pass model output through a shell, and confine file access.
Shell = interpreter of attacker text
If a tool builds a shell command by concatenating model output ("cat " + filename run through a shell), characters such as ;, &&, | and backticks let the attacker chain extra commands. Defences: do not use a shell; call programs with an argument list so each value stays one argument; allow-list which commands and which values are acceptable; run in a sandbox with no credentials. For files, resolve the real path (following .. and symlinks) and check it stays inside an allowed base folder; reject absolute paths. The examples show a shell=True call executing the injected echo, a safe allow-list check, and a path guard that blocks ../ and absolute paths while permitting harmless sub/../notes.txt.
shell=True versus an argument list, run
I ran this with plain Python 3 (standard library only). All attacks here are harmless demonstrations on local data, using no real systems. With shell=True, the text after the semicolon runs as a second command and prints INJECTED. The safe version checks the requested name against an allow-list and runs echo with an argument list, so the whole string is refused (or, for an allowed name, treated as one plain argument). The command here is a harmless echo.
import subprocess
model_output = "report.txt; echo INJECTED"
unsafe = subprocess.run("echo reading " + model_output, shell=True, capture_output=True, text=True)
print("UNSAFE (shell=True):", unsafe.stdout.strip().replace("\n", " | "))
ALLOWED = {"report.txt", "summary.txt"}
def safe_read(name):
if name not in ALLOWED: return f"refused: {name!r} is not an allowed file"
return subprocess.run(["echo", "reading", name], capture_output=True, text=True).stdout.strip() # no shell, argument list
print("SAFE :", safe_read(model_output))
print("SAFE ok:", safe_read("report.txt"))
Output:
UNSAFE (shell=True): reading report.txt | INJECTED SAFE : refused: 'report.txt; echo INJECTED' is not an allowed file SAFE ok: reading report.txt
Confining file paths, run
I ran this with plain Python 3 (standard library only). All attacks here are harmless demonstrations on local data, using no real systems. The guard resolves each path and checks it stays inside the allowed folder. notes.txt and sub/../notes.txt resolve to the same allowed file, while ../secret.txt and /etc/passwd are blocked.
import os, tempfile
def safe_open(base, user_path):
full = os.path.realpath(os.path.join(base, user_path))
if os.path.commonpath([os.path.realpath(base), full]) != os.path.realpath(base):
return "blocked: path escapes the allowed folder"
return "ok: " + os.path.relpath(full, os.path.realpath(base))
with tempfile.TemporaryDirectory() as base:
open(os.path.join(base, "notes.txt"), "w").write("hello")
for p in ("notes.txt", "../secret.txt", "sub/../notes.txt", "/etc/passwd"):
print(f"{p:20} -> {safe_open(base, p)}")
Output:
notes.txt -> ok: notes.txt ../secret.txt -> blocked: path escapes the allowed folder sub/../notes.txt -> ok: notes.txt /etc/passwd -> blocked: path escapes the allowed folder
Prefer library calls over subprocesses
If a Python function can do the job, calling it avoids shells and argument parsing entirely.
Quick check: Why is calling a program with an argument list safer than shell=True?
- It runs faster on GPUs
- Each value stays a single argument and is never interpreted as shell syntax
- It hides the output
- Shells are illegal
Answer
Each value stays a single argument and is never interpreted as shell syntax — Without a shell, characters like ; and && have no special meaning.