VNX-BASH-004 – Unquoted Variable Used in Command or Test
Overview
This rule flags an unquoted expansion of a value the caller of the script controls, in shell scripts (.sh, .bash, .ksh and extensionless files with a sh/bash/dash/ksh shebang). The value is a script-level positional parameter ($1, ${2}, ${1:-x}, $@, $*) or a variable assigned from one, from read, from OPTARG or by for x in "$@" within the 80 lines above. It is reported where the shell splits and globs it: as an argument of a command, or as an operand of [ ] or test. Not reported: double-quoted expansions (including multi-line strings and quoted awk programs), [[ ]], arithmetic, assignments, case words, special parameters ($#, $?, $$, $!), a function’s own positional parameters, variables the script sets itself, echo/printf/exit/return/shift lines, here-document bodies, test paths and .bats files, and zsh scripts (zsh does not split unquoted parameters).
When a variable is unquoted inside [ ], the shell performs word splitting on its value before passing it to the test command. A variable containing a space becomes two separate tokens, causing the test to receive the wrong number of arguments and behave unexpectedly or throw a syntax error. A value containing glob characters such as * or ? triggers filename expansion, potentially matching unintended files from the working directory.
The security impact is more direct when the variable value is attacker-controlled. An adversary who can set a variable to a string with carefully placed spaces or shell metacharacters can manipulate conditional logic, bypass checks, or inject additional arguments into the test command.
Severity: Medium | CWE: CWE-78 – Improper Neutralisation of Special Elements Used in an OS Command
Why This Matters
Argument injection through word splitting is a subtle but serious vulnerability class in shell scripts. Unlike obvious command injections via eval, unquoted variable expansion attacks exploit the shell’s own parsing rules. A file named foo bar in a directory, or an environment variable containing a space set by a malicious actor earlier in a pipeline, can silently change the logic of a security check.
Consider an access-control script that checks a username from an environment variable: if the username is alice admin, the unquoted test [ $USERNAME = "alice" ] becomes [ alice admin = "alice" ], which is a syntax error — the check silently fails open or causes an unhandled exit depending on how errors are handled. Real-world CI/CD systems have been exploited through injected branch names or commit message content that ended up in shell variable comparisons without quoting.
ATT&CK technique T1059.004 covers Unix shell abuse, and CAPEC-88 describes argument injection — both are relevant here because exploiting this pattern can allow an attacker to alter the behaviour of security-relevant conditional checks in scripts.
What Gets Flagged
target_dir=$1
rm -rf $target_dir # FLAGGED: "a b" deletes ./a and ./b
cp $2 /var/backups/ # FLAGGED: positional parameter used unquoted
for f in "$@"; do mv $f /srv/archive/; done # FLAGGED
read -r -p "Log directory: " logdir
find $logdir -name "*.log" -delete # FLAGGED: read input split into arguments
name=$1
if [ -n $name ]; then ... # FLAGGED: always true when $name is empty
Not flagged:
rm -rf "$target_dir"
if [[ -n $target && $target == release ]]; then ...
case $target in release) ;; esac
RSYNC_OPTS="-a --delete"; rsync $RSYNC_OPTS src dst # set by the script itself
copy() { cp $1 /opt/; } # a function's own argument
Remediation
- Always double-quote variable references inside
[ ]tests:[ "$var" = "value" ]. - Prefer
[[ ]]in Bash scripts — it does not perform word splitting or glob expansion on variable expansions, making it safer by default. - Enable
set -uso that any reference to an unset variable is caught immediately rather than silently expanding to empty. - Use ShellCheck as part of your CI pipeline to catch unquoted variable usage automatically.
# SAFE: double-quoted variable in [ ] test
if [ "$username" = "admin" ]; then
grant_access
fi
# SAFE: [[ ]] handles word splitting internally (Bash only)
if [[ $username == "admin" ]]; then
grant_access
fi
# SAFE: quoted and using [[ ]] for file test
if [[ -f "$file_path" ]]; then
process_file
fi