Safeguard
Application Security•

ASVS 5.0 V1.2.5: Stop OS Command Injection

ASVS V1.2.5 requires parameterized OS calls or contextual output encoding. Learn to replace shell strings with argument arrays and verify with real tests.

Safeguard Security Team
4 min read

ASVS 5.0 V1.2.5 (Level 1) requires that your application is protected against OS command injection: operating system calls must pass untrusted data as separate arguments (a parameterized call), or, where that is impossible, the data must be encoded correctly for the command line context. The best fix is to avoid the shell entirely.

Why it matters

When code builds a command string and hands it to a shell, characters such as ;, |, &, backticks and $(...) stop being data and become instructions (CWE-78). A feature like "ping this host" or "convert this image" becomes remote code execution under the application's account. That means access to environment variables, cloud credentials and everything the service can reach.

How to implement it

First ask whether you need a process at all. Most tasks have a library: image resizing, archive handling, DNS lookups, PDF generation. A library call has no shell to inject into.

If you must run a program, pass an argument array and do not invoke a shell.

import subprocess, ipaddress

def ping(host: str) -> str:
    ipaddress.ip_address(host)                     # validate: raises on anything odd
    result = subprocess.run(
        ["/bin/ping", "-c", "1", "--", host],
        capture_output=True, text=True, timeout=5, shell=False, check=False,
    )
    return result.stdout

Node:

import { execFile } from 'node:child_process';

execFile('/usr/bin/convert', [inputPath, '-resize', '200x200', outputPath],
  { timeout: 10000 }, (err, stdout) => { /* handle */ });

Use execFile or spawn, not exec, which starts a shell. In Java, use ProcessBuilder with a list. In Go, exec.Command("convert", args...).

Add the following layers:

  1. Validate with an allowlist (an IP parser, a pattern for filenames, a fixed set of options).
  2. Use -- before user supplied values so a value starting with a dash cannot be read as an option. Argument injection is its own problem: -o and --output flags can write files.
  3. Set a timeout, run with a low privilege account, and prefer absolute paths to executables.

If a shell is truly unavoidable, quote with the language's tool (shlex.quote in Python, escapeshellarg in PHP), and treat that as a last resort.

How to test it

Send 127.0.0.1; id, 127.0.0.1 | id, $(id), a backtick id payload, a newline followed by id, and a filename like --help into every field that reaches a process call. The expected result is a validation error or literal handling. For time based confirmation, use ; sleep 5 and watch response time. Also grep for os.system, shell=True, exec(, Runtime.exec and popen, and review each hit.

Click through it at your own pace, or open it full screen.

Common mistakes

  • Blocklisting semicolons and forgetting pipes, newlines and substitution.
  • Using shell=True "for convenience" with an f string.
  • Validating the hostname but not stopping option injection.
  • Passing user input to tools that run commands themselves, such as tar options, git arguments or curl config files.
  • Calling a wrapper script that uses eval.

Try it yourself

Build a small intentionally vulnerable endpoint in a throwaway project, such as a route that runs ping with a host from the query string using a shell string. Attack it with 127.0.0.1; id and see the output. Then convert it in three steps and retest after each one:

  1. Switch to an argument array without a shell. The injection payload now fails because the whole string is treated as a host name.
  2. Add address validation with a parser, so the request is rejected before any process starts.
  3. Add --, a timeout and a minimal environment.

Doing this once makes the difference between a shell string and an argument array very clear. Also try passing a value that begins with a dash after step 1, to see argument injection for yourself.

A last check for production code: run your service under a user that cannot write outside its data folder and cannot reach the cloud metadata address. If an injection ever gets through, those limits decide how bad it is.

Quick check

Why is an argument array safer than a command string?

With an array, each value reaches the program as one argument. No shell parses it, so metacharacters have no special meaning.

Why add a double dash before user values?

It tells most programs that everything after it is a positional value, which blocks a user value like -o from being read as an option.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.