0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

防止命令与参数注入

为不可信输入添加引号、进行验证并通过数组传递,从而消除单词拆分和基于 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 防止命令与参数注入
  2. 安全处理机密信息与保持环境整洁
  3. 最小权限执行与 sudo 规范
  4. 使用 ShellCheck 进行静态分析与审计
← 返回 Linux Command Line & Bash Scripting Mastery