防止命令与参数注入
为不可信输入添加引号、进行验证并通过数组传递,从而消除单词拆分和基于 eval 的注入。
防止命令与参数注入 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
Bash 中注入攻击为何会发生
Bash 是一种功能强大的胶水语言——它会将文本直接传递给内核、其他程序和子 Shell。一旦不可信输入未经验证或引用就到达命令,这种能力就会变成安全隐患。
几乎所有 Bash 注入都由两个根本原因导致:
- 单词拆分:未加引号的变量会在空白字符(
IFS)处分割,从而将一个逻辑值变成多个 Shell 令牌。 - 通配符展开:
*、?和[等字符会在命令实际运行之前由 Shell 展开。
如果攻击者能够控制文件名、用户名、URL 参数或环境变量,就可以利用这两点运行任意命令、读取文件或提升权限。
本课程将准确展示这些漏洞是如何出现的,更重要的是,说明如何通过正确的引用、输入验证以及基于数组的参数传递来消除它们。
单词拆分:无声的威胁
当 Bash 遇到未加引号的变量时,会按照 $IFS 中列出的字符拆分其值(默认是空格、制表符和换行符)。看似一个参数的内容会变成多个参数。
运行下面的脚本,观察包含空格的文件名如何变成传递给 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):"
ls始终加引号:Bash 防御的第一条规则
防止单词拆分最简单、最有效的方法是始终为变量展开添加双引号。
"$var"—— 始终展开为一个令牌,并保留空格、制表符和换行符。'literal'—— 单引号:完全不进行展开,适用于固定字符串。- 除非确实需要单词拆分和通配符展开,否则不要使用未加引号的
$var。
下面的脚本展示了上一个示例的安全版本。
#!/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):'
ls通配符注入:当 * 变成武器
未加引号的变量还会受到路径名展开(通配符展开)的影响。如果用户控制的输入包含 * 或 ?,Bash 会在命令运行前根据文件系统对其进行展开。
一种经典的攻击方式是:网页表单将 PATTERN=* 设为参数,而脚本运行 cp $PATTERN /tmp/leak/,从而复制当前目录中的所有文件。
修复方法相同:为变量添加双引号。加引号的 "$PATTERN" 会按字面值传递,Shell 不会对其执行通配符展开。
#!/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_dst未加引号的位置参数导致的参数注入
从调用者处接收参数的脚本是注入攻击的重点目标。每个位置参数($1、$2、……)在使用时都必须加引号。
一种尤其危险的写法是将未加引号的 $@ 或 $* 传递给另一个命令:
"$@"—— 将每个位置参数展开为单独且分别加引号的单词。始终使用这种形式。- 未加引号的
$@或$*—— 会受到单词拆分和通配符展开的影响。 "$*"—— 将所有参数合并为一个单词(通常不是您想要的结果)。
#!/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"通过 eval 和未验证输入进行命令注入
eval 会将其参数重新解析为 Shell 代码。任何进入 eval 的不可信数据都可能执行任意命令。
常见的危险写法:
eval "$user_input"eval echo \$$var(间接变量查找)- 将用户数据传递给
bash -c "$input"
规则:绝不要将不可信输入传递给 eval 或 bash -c。请使用安全的 Bash 替代方案:
- 间接展开:使用
${!varname},而不是eval echo \$$varname - 使用关联数组进行动态键值查找
- 使用函数,而不是生成的命令字符串
#!/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
fi输入验证:优先使用允许列表,而不是拒绝列表
拒绝已知的危险字符(即使用拒绝列表)并不可靠——攻击者可以找到您遗漏的编码方式或字符。相反,应使用允许列表:只接受您确认安全的字符。
Bash 中的允许列表策略:
- 正则匹配:
[[ "$input" =~ ^[A-Za-z0-9_-]+$ ]] - 模式匹配:
case "$input" in [A-Za-z0-9]*) ... ;; esac - 枚举检查:将输入与一组固定的有效值进行比较
请在边界处进行验证——也就是输入刚进入脚本时,确保它在接触任何命令之前就已通过验证。
#!/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' # REJECTED使用数组安全地传递参数
当您需要动态构建命令(例如有条件地添加选项、遍历输入)时,请使用 Bash 数组,而不是拼接字符串。
字符串拼接会丢失所有结构;Bash 数组则会将每个参数保留为独立元素,不会被 Shell 重新解析。
- 声明:
args=() - 追加:
args+=(--flag "$value") - 执行:
command "${args[@]}"
"${args[@]}" 会将每个元素展开为单独且分别加引号的单词——与 "$@" 完全相同。
#!/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[@]}"-- 分隔符:防止选项注入
即使参数已正确加引号,只要它以 - 开头,也可能被误解为选项标志。请考虑 rm "$file",其中 file='-rf .':加引号可以防止单词拆分,但 rm 仍会将 -rf 解释为选项。
POSIX 约定中的 -- 会向大多数 GNU/BSD 工具表示选项结束。-- 后面的所有内容都会被视为位置参数,而不会被当作选项。
#!/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_target为 SQL 和外部工具清理输入
当 Bash 脚本调用数据库命令行工具(psql、mysql)、使用用户提供的 URL 调用 curl 或调用类似工具时,还需要采取两个额外层面的措施:
- 参数化查询:绝不要将用户数据插入 SQL 字符串。请通过
psql中的-v或curl中的--data-urlencode传递值。 - 分离数据与代码:使用带字面格式字符串的
printf;绝不要让用户输入充当格式字符串。
下面的示例会安全地查询 PostgreSQL,使用户提供的值完全不会进入 SQL 文本。
#!/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"加固检查清单:综合运用
生产级安全 Bash 脚本会将本课程介绍的所有技巧结合起来,形成一致的分层防御。下面是一个简洁但完整的加固模板:
set -euo pipefail—— 出错时退出,将未设置的变量视为错误,并传递管道失败。- 在入口处验证 —— 在任何外部输入接触命令之前,都使用允许列表进行验证。
- 为所有内容加引号 ——
"$var"、"$@"、"${array[@]}",除非需要拆分,否则没有例外。 - 使用数组动态构建命令。
- 传递用户提供的文件名或字符串时,为参数添加
--前缀。 - 绝不要将不可信数据与
eval一起使用;间接查找时优先使用${!var}。 - 限制权限 —— 以所需的最低权限运行脚本;对于接收用户输入的脚本,避免在其中使用
sudo。
#!/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[@]}"知识检查:引用与注入防护
检验您对本课程关键概念的理解。
课程回顾:防止命令注入和参数注入
您已经学习了安全处理 Bash 输入的完整防御工具集:
- 单词拆分和通配符展开是将不安全变量变成注入途径的根本机制。
- 为每个变量添加双引号(
"$var"、"$@"、"${arr[@]}"),以抑制这两种威胁。 - 转发参数时使用
"$@"——绝不要使用未加引号的$@或$*。 - 为用户提供的文件名添加
--前缀,以防止选项注入。 - 在所有外部输入到达任何命令之前,使用正则保护(
[[ $v =~ ^pattern$ ]])通过允许列表进行验证。 - 使用数组构建动态命令(
cmd+=()→"${cmd[@]}"),绝不要拼接字符串。 - 移除
eval和bash -c "$input";使用${!varname}进行安全的间接展开。 - 始终以
set -euo pipefail和IFS=$'\n\t'开头,为脚本建立加固基线。
如果从每个脚本的第一行开始始终如一地应用这些实践,就能将 Bash 针对注入类漏洞的攻击面降至接近于零。
常见问题解答
「防止命令与参数注入」课时是免费的吗?
是的 — 「防止命令与参数注入」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
「防止命令与参数注入」这节课中我会学到什么?
为不可信输入添加引号、进行验证并通过数组传递,从而消除单词拆分和基于 eval 的注入。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「防止命令与参数注入」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?
能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。