Preventing Command and Argument Injection
Quote, validate, and array-pass untrusted input to eliminate word-splitting and eval-based injection.
Preventing Command and Argument Injection is a free Linux Command Line & Bash Scripting Mastery lesson on CoddyKit. This is lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Linux Command Line & Bash Scripting Mastery learning path, and your progress syncs across the web and the CoddyKit app. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
Why Injection Attacks Happen in Bash
Bash is a powerful glue language — it passes text directly to the kernel, other programs, and sub-shells. That power becomes a liability the moment untrusted input reaches a command without validation or quoting.
Two root causes drive nearly every Bash injection:
- Word splitting: Unquoted variables are split on whitespace (
IFS), turning one logical value into many shell tokens. - Glob expansion: Characters like
*,?, and[are expanded by the shell before the command even runs.
An attacker who controls a filename, username, URL parameter, or environment variable can exploit both to run arbitrary commands, read files, or escalate privileges.
This lesson shows exactly how these vulnerabilities appear and — more importantly — how to eliminate them with correct quoting, input validation, and array-based argument passing.
Word Splitting: The Silent Threat
When Bash sees an unquoted variable, it splits its value on any character listed in $IFS (default: space, tab, newline). What looks like one argument becomes many.
Run the script below and observe how a filename with a space becomes two separate arguments to rm.
#!/usr/bin/env bash
# Dangerous: unquoted variable
FILE='important file.txt'
# Create the file so the demo is self-contained
touch "$FILE"
echo "Files before:"
ls
# BUG: rm sees TWO arguments: 'important' and 'file.txt'
# If 'important' does not exist, rm prints an error but continues.
rm $FILE # <-- unquoted, word-split happens here
echo "Files after (unquoted rm):"
lsAlways Quote: The First Rule of Defensive Bash
The simplest and most effective defense against word splitting is to always double-quote variable expansions.
"$var"— expands to exactly one token, preserving spaces, tabs, and newlines.'literal'— single quotes: no expansions at all, useful for fixed strings.- Never use
$varunquoted unless you explicitly need word splitting and glob expansion.
The script below shows the safe version of the previous example.
#!/usr/bin/env bash
set -euo pipefail
FILE='important file.txt'
touch "$FILE"
echo 'Files before:'
ls
# SAFE: double-quotes keep the filename as one token
rm "$FILE"
echo 'Files after (quoted rm):'
lsGlob Injection: When * Becomes a Weapon
Unquoted variables are also subject to pathname expansion (globbing). If user-controlled input contains * or ?, Bash expands it against the filesystem before the command runs.
A classic attack vector: a web form that sets PATTERN=* and the script runs cp $PATTERN /tmp/leak/ — copying every file in the current directory.
The fix is identical: double-quote the variable. A quoted "$PATTERN" is passed literally; the shell never performs glob expansion on it.
#!/usr/bin/env bash
set -euo pipefail
# Simulate attacker-supplied input
PATTERN='*'
mkdir -p /tmp/safe_demo_src /tmp/safe_demo_dst
touch /tmp/safe_demo_src/secret1.txt /tmp/safe_demo_src/secret2.txt
cd /tmp/safe_demo_src
# UNSAFE: glob expands, copies every file
# cp $PATTERN /tmp/safe_demo_dst/
# SAFE: pattern is treated as a literal filename
cp "$PATTERN" /tmp/safe_demo_dst/ 2>&1 || echo 'No file named literally "*" — attack neutralised'
rm -rf /tmp/safe_demo_src /tmp/safe_demo_dstArgument Injection via Unquoted Positional Parameters
Scripts that accept arguments from the caller are prime injection targets. Each positional parameter ($1, $2, ...) must be quoted wherever it is used.
A particularly dangerous pattern is passing $@ or $* unquoted to another command:
"$@"— expands each positional parameter as a separate, individually quoted word. Always use this form.$@or$*unquoted — subject to word splitting and globbing."$*"— joins all parameters into one word (rarely what you want).
#!/usr/bin/env bash
set -euo pipefail
# Safe wrapper: forward all arguments quoted
grep_wrapper() {
local pattern="$1"
shift
# "$@" preserves each file argument as one token
grep -rn "$pattern" "$@"
}
# Usage: ./script 'error msg' /var/log/syslog '/path with spaces/app.log'
echo 'Searching current script for "safe":'
grep_wrapper 'safe' "$0"Command Injection via eval and Unvalidated Input
eval re-parses its argument as shell code. Any untrusted data reaching eval can execute arbitrary commands.
Common dangerous patterns:
eval "$user_input"eval echo \$$var(indirect variable lookup)- Passing user data through
bash -c "$input"
Rule: never pass untrusted input to eval or bash -c. Use safe Bash alternatives:
- Indirect expansion:
${!varname}instead ofeval echo \$$varname - Associative arrays for dynamic key-value lookup
- Functions instead of generated command strings
#!/usr/bin/env bash
set -euo pipefail
# Simulated attacker-supplied variable name
VARNAME='PATH; echo INJECTED'
# UNSAFE: eval lets attacker run 'echo INJECTED'
# eval "echo \$$VARNAME"
# SAFE: indirect expansion only resolves valid variable names
# First validate that VARNAME is a legal identifier
if [[ "$VARNAME" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "Value: ${!VARNAME}"
else
echo "ERROR: invalid variable name: '$VARNAME'" >&2
exit 1
fiInput Validation: Allowlists Over Denylists
Rejecting known-bad characters (a denylist) is fragile — attackers find encodings or characters you forgot. Instead, allowlist: accept only characters you know are safe.
Allowlist strategies in Bash:
- Regex match:
[[ "$input" =~ ^[A-Za-z0-9_-]+$ ]] - Pattern match:
case "$input" in [A-Za-z0-9]*) ... ;; esac - Enum check: compare against a fixed set of valid values
Validate at the boundary — as soon as input enters the script — before it touches any command.
#!/usr/bin/env bash
set -euo pipefail
validate_username() {
local name="$1"
# Allowlist: only lowercase letters, digits, underscore, hyphen; 1-32 chars
if [[ ! "$name" =~ ^[a-z0-9_-]{1,32}$ ]]; then
echo "ERROR: invalid username '${name}'" >&2
return 1
fi
echo "Username accepted: $name"
}
validate_username 'alice' # OK
validate_username 'bob_smith-2' # OK
validate_username 'root; rm -rf /' # REJECTED
validate_username '../etc/passwd' # REJECTEDUsing Arrays to Pass Arguments Safely
When you need to build a command dynamically — conditionally adding flags, looping over inputs — use a Bash array instead of concatenating strings.
String concatenation collapses all structure; a Bash array preserves each argument as a discrete element with no shell re-parsing.
- Declare:
args=() - Append:
args+=(--flag "$value") - Execute:
command "${args[@]}"
"${args[@]}" expands every element as a separate, individually quoted word — exactly like "$@".
#!/usr/bin/env bash
set -euo pipefail
# Build a find command safely with an array
BASEDIR='/tmp'
USER_PATTERN='*.log' # could come from user input (validate first!)
MAX_DAYS=7
cmd=(find "$BASEDIR" -type f -name "$USER_PATTERN" -mtime "+${MAX_DAYS}")
# Optionally add -delete only when requested
DELETE=false
if [[ "$DELETE" == 'true' ]]; then
cmd+=(-delete)
fi
echo "Running: ${cmd[*]}"
"${cmd[@]}"The -- Separator: Protecting Against Flag Injection
Even a correctly quoted argument can be misinterpreted as an option flag if it begins with -. Consider rm "$file" where file='-rf .': the quoting protects word splitting, but rm still interprets -rf as flags.
The POSIX convention -- signals end of options to most GNU/BSD utilities. Everything after -- is treated as a positional argument, never as a flag.
#!/usr/bin/env bash
set -euo pipefail
# Simulate a dangerous filename supplied by the user
FILENAME='-rf /tmp/safe_demo_target'
mkdir -p /tmp/safe_demo_target
touch /tmp/safe_demo_target/keep_me.txt
echo 'Files before:'
ls /tmp/safe_demo_target/
# UNSAFE (flag injection — do NOT uncomment in real systems):
# rm "$FILENAME"
# SAFE: -- ends option processing; filename is treated literally
rm -- "$FILENAME" 2>&1 || echo "No such file (attack neutralised): $FILENAME"
echo 'Files after:'
ls /tmp/safe_demo_target/
rm -rf /tmp/safe_demo_targetSanitising Input for SQL and External Tools
When Bash scripts invoke database CLIs (psql, mysql), curl with user-supplied URLs, or similar tools, two additional layers apply:
- Parameterised queries: Never interpolate user data into SQL strings. Pass values via
-vinpsqlor--data-urlencodeincurl. - Separate data from code: Use
printfwith a literal format string; never let user input be the format string.
The example below queries PostgreSQL safely, keeping the user-supplied value completely out of the SQL text.
#!/usr/bin/env bash
set -euo pipefail
# Validate first: only allow alphanumeric usernames
USERNAME="${1:-alice}"
if [[ ! "$USERNAME" =~ ^[a-z0-9_]{1,32}$ ]]; then
echo 'ERROR: invalid username' >&2
exit 1
fi
# UNSAFE:
# psql -c "SELECT * FROM users WHERE name = '$USERNAME';"
# SAFE: pass value as a psql variable, never inside the SQL text
# psql -v username="$USERNAME" -c 'SELECT * FROM users WHERE name = :username;'
# Demonstrate the principle with printf (safe format string usage)
printf 'Query would use username: %s\n' "$USERNAME"Hardening Checklist: Putting It All Together
A production-grade secure Bash script combines every technique from this lesson into a consistent, layered defence. Here is a minimal but complete hardened template:
set -euo pipefail— exit on error, treat unset variables as errors, propagate pipe failures.- Validate at entry — allowlist every external input before it touches any command.
- Quote everything —
"$var","$@","${array[@]}"— no exceptions unless you need splitting. - Use arrays for dynamic command construction.
- Prefix arguments with
--when passing user-supplied filenames or strings. - Never use
evalwith untrusted data; prefer${!var}for indirect lookup. - Restrict permissions — run scripts with the minimum required privileges; avoid
sudoinside scripts that accept user input.
#!/usr/bin/env bash
# Hardened template — safe argument injection prevention
set -euo pipefail
IFS=$'\n\t'
#--- 1. Validate inputs at the boundary ---
SEARCH_DIR="${1:-}"
PATTERN="${2:-}"
[[ -z "$SEARCH_DIR" || -z "$PATTERN" ]] && { echo 'Usage: script <dir> <pattern>' >&2; exit 1; }
[[ ! "$SEARCH_DIR" =~ ^[A-Za-z0-9/_.-]+$ ]] && { echo 'ERROR: unsafe directory path' >&2; exit 1; }
[[ ! "$PATTERN" =~ ^[A-Za-z0-9._-]+$ ]] && { echo 'ERROR: unsafe pattern' >&2; exit 1; }
#--- 2. Build command with an array ---
cmd=(find -- "$SEARCH_DIR" -type f -name "$PATTERN")
#--- 3. Execute — no string interpolation, no eval ---
echo "Executing: ${cmd[*]}"
"${cmd[@]}"Knowledge Check: Quoting and Injection Prevention
Test your understanding of the key concepts in this lesson.
Lesson Recap: Preventing Command and Argument Injection
You have covered the complete defensive toolkit for safe Bash input handling:
- Word splitting and glob expansion are the root mechanisms that turn unsafe variables into injection vectors.
- Double-quote every variable (
"$var","$@","${arr[@]}") to suppress both threats. - Use
"$@"— never$@or$*unquoted — when forwarding arguments. - Prefix user-supplied filenames with
--to prevent flag injection. - Allowlist all external input with a regex guard (
[[ $v =~ ^pattern$ ]]) before it reaches any command. - Build dynamic commands with arrays (
cmd+=()→"${cmd[@]}"), never string concatenation. - Eliminate
evalandbash -c "$input"; use${!varname}for safe indirect expansion. - Always open scripts with
set -euo pipefailandIFS=$'\n\t'for a hardened baseline.
These practices, applied consistently from the first line of every script, reduce the attack surface of Bash to near zero for injection-class vulnerabilities.
Learn Bash with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 22
- Lessons
- 88
Frequently Asked Questions
Is the “Preventing Command and Argument Injection” lesson free?
Yes — the full text of “Preventing Command and Argument Injection” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Linux Command Line & Bash Scripting Mastery course, upgrade to CoddyKit PRO. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
What will I learn in “Preventing Command and Argument Injection”?
Quote, validate, and array-pass untrusted input to eliminate word-splitting and eval-based injection. You practise Linux Command Line & Bash Scripting Mastery with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Linux Command Line & Bash Scripting Mastery?
No prior experience is required. Linux Command Line & Bash Scripting Mastery on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 1 of 4.
How long does the “Preventing Command and Argument Injection” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Linux Command Line & Bash Scripting Mastery lesson?
Yes. Every Linux Command Line & Bash Scripting Mastery lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Preventing Command and Argument Injection
- Secure Secret Handling and Environment Hygiene
- Least-Privilege Execution and sudo Discipline
- Static Analysis and Auditing with ShellCheck