Safeguard
Application Security•

CWE-88 Argument Injection: How It Happens and How to Fix It

Even without a shell, an attacker who adds an extra option to a command can change what it does. This guide covers CWE-88, with vulnerable and fixed code.

Safeguard Security Team
5 min read

CWE-88 is argument injection. The product builds a string for a command run by a separate component in another control sphere, but does not properly delimit the intended arguments, options or switches within that string.

It affects code that launches other programs, and tools that wrap command line utilities such as Git, archivers and network clients. It is easy to miss because the code may already be safe against shell separators.

How it happens

When commands are built by interpolating into a string, developers often assume only the arguments they wrote will be processed. That assumption can be even stronger when the code blocks shell metacharacters, so a second command cannot be added. But the code still uses whitespace or other delimiters to separate arguments. If an attacker supplies input that contains argument-separating delimiters, the command ends up with more arguments than intended, and the extra ones can change its behavior. The effect depends on what the extra options support.

A short Python example, with a Git wrapper:

import subprocess

# Vulnerable: no shell, but the value can be read as an option
def show(ref):
    return subprocess.run(["git", "show", ref], capture_output=True)

# ref = "--output=/tmp/x" is treated as an option, not a revision

One fix is to refuse values that begin with a dash and validate the shape of what is expected. A pattern check like this one also rejects values that begin with a dash:

import re

def show(ref):
    if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,100}", ref):
        raise ValueError("bad ref")
    return subprocess.run(["git", "show", ref], capture_output=True)

The observed examples in the record:

  • CVE-2022-36069: a Python dependency management tool avoids OS command injection when generating Git commands but allows injection of optional arguments with input beginning with a dash.
  • CVE-1999-0113: the canonical example, where the "-froot" argument is passed on to another program, where the "-f" causes execution as user root.
  • CVE-2001-0150: a web browser executes Telnet sessions using command line arguments specified by the web site, which could allow remote attackers to execute arbitrary commands.
  • CVE-2001-0667: a web browser lets remote attackers execute commands by spawning Telnet with a log file option.

Gold pages: CVE-2022-36069, CVE-1999-0113, CVE-2001-0150 and CVE-2001-0667.

What an attacker gains

MITRE says an attacker could include arguments that allow unintended commands or code to be executed, allow sensitive data to be read or modified, or cause other impacts that depend on what the extra arguments do. The listed effects cover confidentiality, integrity, availability, altered execution logic and reading and changing application data.

How to find it

MITRE lists automated static analysis (SAST), which can find some instances by modeling data flow in source or compiled code without running it. It also recommends dynamic testing: large suites of diverse inputs, fuzzing, robustness testing and fault injection. The product may slow down, but it should not become unstable, crash or give wrong results.

A manual check is simple. For each call that launches a program, list which parts of the argument list come from outside, then ask what each tool does with an input that starts with a dash.

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

How to fix it

From the record:

  1. Pass arguments separately. Avoid building one string with the command and its arguments. Use functions that take independent arguments, such as an array, which perform quoting for you. MITRE's example: in PHP, escapeshellarg() escapes a single argument, or exec() can take an array of arguments. In C, refactor from system(), which takes a single string, to exec functions with separate arguments. MITRE rates this high effectiveness.
  2. Know your inputs and validate at defined interfaces. List every place untrusted input enters: parameters, cookies, network data, environment variables, headers, URL parts, email, files, databases and external systems.
  3. Use an allowlist. Accept known good input and reject the rest, looking at length, type, range, syntax and business rules. Denylists alone miss things.
  4. Convert to the expected type, such as a number, then check the range and consistency across fields.
  5. Decode and canonicalize before validating, and make sure the same input is not decoded twice. MITRE suggests repeated canonicalization until the input stops changing.
  6. Use the same character encoding across components, and set it explicitly when the protocol lets you.
  7. Validate after combining sources. Pieces can each pass and still violate the rules when joined.
  8. Test with fuzzing and similar dynamic techniques.

If a dependency builds commands, the flaw may be in code you did not write. The free in-browser dependency scan at gold.safeguard.sh/scan needs no login, keeps the file on your machine and covers up to 100 dependencies. Reachability analysis then tells you whether your code calls the vulnerable function.

Quick check

If I avoid the shell, am I safe from CWE-88?

Not fully. A value that begins with a dash can still be read as an option by the program you start. Validate the shape of the value as well.

What is the highest-rated fix in the MITRE record?

Parameterization: pass arguments as separate items instead of one command string.

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.