# Command Injection and Path Traversal — LLM Application Security

Source: https://www.geekswithgeeks.com/en/llm-security/o-shell

> 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`.

```python
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.

```python
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.

**Quiz:** Why is calling a program with an argument list safer than shell=True?

- [ ] It runs faster on GPUs
- [x] 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.
