Chapter 18Lesson 01~100 minutes

Command Injection and Untrusted Input

Bash is powerful because text can become code, which is exactly why untrusted input needs a strict boundary. Safe automation keeps external values as data and avoids reparsing them as shell syntax.

IntermediateSecurity & portabilityHands-on lab

Learning objectives

By the end of this lesson

  • Identify shell-code injection boundaries.
  • Avoid eval and generated command strings.
  • Use arrays for dynamic arguments.
  • Validate identifiers and paths by domain rules.
  • Protect option and remote-command boundaries.

1. Every external value crosses a trust boundary

Arguments, environment variables, API fields, filenames, Git metadata, CI inputs, config values, and remote responses are all data. The danger begins when data is reinterpreted as shell syntax.

Data versus shell source
flowchart TD
  U["untrusted data"] --> V["validation / normalization"]
  V --> A["argument boundary"]
  A --> C["fixed command"]
  U -. "unsafe interpolation" .-> S["shell source"]
Primary rule

Do not construct executable shell source from untrusted strings when you can pass them as ordinary arguments.

2. eval turns data into code

user_input='$(touch /tmp/injected)'

# Dangerous:
# eval "printf '%s\n' $user_input"

# Safe:
printf '%s\n' "$user_input"

eval asks Bash to parse text again. If untrusted content reaches that second parse, shell metacharacters become executable syntax.

3. bash -c and sh -c create the same class of risk

name='api; echo injected'

# Dangerous:
# bash -c "deploy $name"

# Safer:
deploy "$name"

A shell command string is code. Prefer direct command invocation with separate arguments.

4. Use arrays when commands need dynamic argument lists

args=(--timeout 30 --retries 3)

if [[ $verbose == true ]]; then
  args+=(--verbose)
fi

curl "${args[@]}" "$url"

Arrays preserve argument boundaries without asking the shell to reparse a generated string.

5. Validate identifiers against the domain you actually support

service=${1:-}

[[ $service =~ ^[a-z0-9][a-z0-9.-]{0,62}$ ]] || {
  printf 'invalid service name: %q\n' "$service" >&2
  exit 65
}

Validation should express business constraints, not merely strip a few dangerous characters.

6. Leading-dash input can become an option

file=$1

rm -- "$file"
grep -- "$pattern" "$file"

The -- end-of-options marker prevents a pathname such as -rf from being interpreted as flags when the utility supports it.

7. Treat user strings as printf data, not format strings

message=$1

# Safe:
printf '%s\n' "$message"

# Risky when message is untrusted:
# printf "$message\n"

Untrusted format strings can trigger unexpected conversions or errors. Keep the format constant.

8. Regex, jq, SQL, YAML, and remote shells have their own injection boundaries

Command injection is only one interpreter problem. If Bash passes untrusted text into a regular expression, jq program, SQL statement, YAML template, or remote shell command, use that tool's supported parameter mechanism instead of source concatenation.

9. Remote command strings multiply parsing risk

validated_name=$1

[[ $validated_name =~ ^[a-z0-9.-]+$ ]] || exit 65

ssh host /usr/local/bin/restart-service "$validated_name"

Prefer a fixed remote program plus validated arguments over a locally assembled remote shell script.

10. Path traversal is different from command injection

base=/srv/artifacts
name=${1:-}

[[ $name != */* && $name != "." && $name != ".." ]] || {
  printf 'invalid artifact name\n' >&2
  exit 65
}

target="$base/$name"

Even perfectly quoted data can still violate path policy. Quoting prevents shell interpretation; validation enforces domain rules.

11. Hands-on lab: safe command construction

mkdir -p "$HOME/devops-academy/bash/chapter18/lesson01"
cd "$HOME/devops-academy/bash/chapter18/lesson01"

cat > safe-run.sh <<'EOF'
#!/usr/bin/env bash
set -u

name=${1:-}
[[ -n $name ]] || {
  printf 'usage: %s NAME\n' "$0" >&2
  exit 64
}

[[ $name =~ ^[a-zA-Z0-9._-]+$ ]] || {
  printf 'invalid name=%q\n' "$name" >&2
  exit 65
}

args=(--name "$name")
if [[ ${VERBOSE:-false} == true ]]; then
  args+=(--verbose)
fi

printf 'would invoke tool with arguments:\n'
printf '  %q\n' "${args[@]}"
EOF

chmod u+x safe-run.sh
./safe-run.sh api
./safe-run.sh 'api;touch /tmp/nope' || true

Verification checklist

12. Knowledge check

Question 1. Why is eval dangerous with untrusted input?

Question 2. What should you use for dynamic argument lists?

Question 3. Does quoting replace validation?

Question 4. Why use -- before a filename?

13. Summary

Keep untrusted data as data. Pass arguments directly, use arrays, validate against domain rules, terminate options with --, and avoid second-stage interpreters such as eval or bash -c unless the input is fully controlled.

14. Further reading

  • GNU Bash Reference Manual — quoting, arrays, and eval.
  • ShellCheck documentation on quoting and eval patterns.
  • OWASP Command Injection guidance.
  • POSIX utility syntax guidelines for option termination.
Next lesson

Secrets, Credentials, and Process-Table Exposure

Continue Chapter 18 by strengthening Bash trust boundaries, security assumptions, and portability guarantees.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.