VNX-BASH-007 – Unquoted Command Substitution in Shell Script
Overview
This rule flags lines in Bash and shell scripts (.sh, .bash, .bats) where a $(...) command substitution appears without enclosing double-quotes. The detection looks for $(...) that is not immediately preceded by a double-quote character and is not part of a variable assignment or another $ expression. Lines that are commented out are excluded, as are lines where the command substitution is already quoted with "$(...".
When command output is captured without double-quotes, the shell applies word splitting (splitting on whitespace or the current IFS) and glob expansion (expanding *, ?, and [...] patterns) to the result before passing it to the enclosing command. A command that produces a filename with spaces, a path with glob characters, or newline-separated output will be split into multiple unexpected arguments.
If the output of the substituted command is at all influenced by attacker-controlled data — a filename from a directory listing, a value from a configuration file, or the output of a tool that processes user input — word splitting can turn that single value into multiple shell arguments, potentially injecting additional flags or operands.
Severity: Medium | CWE: CWE-88 – Improper Neutralisation of Argument Delimiters in a Command
Why This Matters
Argument injection via unquoted command substitution is a common source of bugs in shell scripts, and it becomes a security issue whenever the substituted command’s output can be influenced by an external party. Consider a script that passes the output of a basename or git rev-parse call into another command without quoting — an attacker who can create a file or branch with a specially crafted name containing spaces or shell metacharacters can inject extra arguments into the receiving command.
Real-world attacks of this form have been demonstrated in CI/CD pipelines where attacker-controlled branch names or tag names flow through shell scripts. GitHub Actions workflows that pass ${{ github.head_ref }} or similar values into shell scripts are particularly vulnerable when the script then uses unquoted command substitutions that incorporate those values. ATT&CK T1059.004 covers Unix shell interpreter abuse including this injection class.
The fix is mechanical and has zero performance cost: simply add double-quotes around every $(...). If the script genuinely needs to split the output of a command into an array, use mapfile -t arr < <(command) or read -ra arr <<< "$(command)" instead of relying on unquoted word splitting.
What Gets Flagged
# FLAGGED: unquoted command substitution in assignment used in command
files=$(ls /tmp)
rm $files # $files unquoted and subject to splitting/glob
# FLAGGED: unquoted $() passed directly to a command
chown $(id -u):$(id -g) /some/path
# FLAGGED: command substitution in test without quotes
if [ $(whoami) = "root" ]; then echo ok; fi
Remediation
- Always wrap
$(...)in double-quotes:"$(command)". - When you need the output split into an array, use
mapfileorread -rarather than relying on unquoted word splitting. - Enable ShellCheck in your editor and CI pipeline to catch unquoted substitutions automatically.
- Prefer
[[ ]]over[ ]for conditional tests — while quoting is still recommended,[[ ]]does not perform word splitting.
# SAFE: command substitution double-quoted
files="$(find /tmp -maxdepth 1 -name '*.log')"
echo "Found: $files"
# SAFE: owner assignment with quoted substitutions
chown "$(id -u)":"$(id -g)" /some/path
# SAFE: comparison with quoted substitution
if [ "$(whoami)" = "root" ]; then echo ok; fi
# SAFE: split into array explicitly instead of relying on word splitting
mapfile -t log_files < <(find /var/log -name "*.log")
for f in "${log_files[@]}"; do
process "$f"
done