Safeguard
Application Security•

CWE-146 Improper Neutralization of Expression/Command Delimiters

A semicolon or pipe in input can end one command and start another. See how CWE-146 leads to command execution and how to avoid building command strings.

Safeguard Security Team
2 min read

CWE-146 applies when the product receives input from an upstream component but does not neutralize, or incorrectly neutralizes, special elements that could be read as expression or command delimiters by a downstream component. It is a variant under CWE-140, and it is language-neutral. MITRE's record lists no observed CVEs.

How it happens

Shells, query languages and expression engines use characters to end one statement and begin another: a semicolon, an ampersand, a pipe, a newline. If user input is placed into such a statement and the delimiter is not neutralized, the attacker can end the intended command and append their own.

# Vulnerable: input ends up inside a shell command
os.system("ping -c 1 " + host)
# host = "example.com; cat /etc/passwd" runs two commands

As MITRE puts it, an injected, absent or malformed delimiter may cause the process to take unexpected actions. This is the delimiter-level view of the broader injection problem described in CWE-138.

What an attacker gains

Unlike its siblings, MITRE lists this one with a wide impact. Consequences include execution of unauthorized code or commands and altered execution logic, affecting confidentiality, integrity and availability. In the example above, any command the process can run is available to the attacker.

How to find it

  • Search for calls that run shell commands, evaluate expressions or build queries from strings.
  • Test inputs containing semicolons, ampersands, pipes, backticks and newlines, and check whether anything extra runs.
  • Use static analysis rules that follow untrusted data to command execution sinks.

How to fix it

  1. Do not use a shell. Call programs with an argument array so delimiters have no meaning.
  2. Use parameterized queries and prepared statements for databases.
  3. Validate input against a strict allow list, for example a hostname pattern.
  4. Where an expression engine is needed, give it data through variables, not by pasting text into the expression.
  5. Run processes with the least privilege possible.
# Fixed: no shell, validated input
import re, subprocess
if not re.fullmatch(r"[A-Za-z0-9.-]{1,253}", host):
    raise ValueError("bad host")
subprocess.run(["ping", "-c", "1", host], check=True)

Quick check

Why does an argument array stop this?

No shell parses the string, so a semicolon is just a character inside one argument and cannot end the command.

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.