eval命令非常强大,但也非常容易被滥用。
它会导致代码被解析两次而不是一次。这意味着,如果你的代码中包含变量引用,shell解析器将评估该变量的内容。如果变量包含一个shell命令,shell可能会运行该命令,无论你是否希望运行它。这可能会导致意外的结果,特别是当变量可以从不受信任的来源(如用户或用户创建的文件)读取时。
请注意,eval命令在编程中被广泛认为是危险的。它可以执行任意的Shell代码,包括恶意代码,因此应该谨慎使用。
Bash 4.3引入了declare -n("名称引用")来模仿Korn shell的nameref特性,允许变量保存对其他变量的引用。然而,Bash中使用的实现存在一些问题。
首先,Bash的declare -n实际上并没有避免名称冲突问题:
$ foo() { declare -n v=$1; }
$ bar() { declare -n v=$1; foo v; }
$ bar v
bash: warning: v: circular name reference
换句话说,我们无法给名称引用指定一个安全的名称。如果调用者的变量恰好具有相同的名称,那就麻烦了。
其次,Bash的名称引用实现仍然允许任意代码执行:
$ foo() { declare -n var=$1; echo "$var"; }
$ foo 'x[i=$(date)]'
bash: i=Thu Mar 27 16:34:09 EDT 2034: syntax error in expression (error token is "Mar 27 16:34:09 EDT 2023")
这个例子并不优雅,但你可以清楚地看到date命令实际上被执行了。这绝不是我们想要的结果。
尽管存在这些缺点,declare -n特性是朝着正确方向迈出的一步。但你必须小心选择一个调用者不会使用的名称(这意味着你需要对调用者有某种控制,即使只是告诉他们“不要使用以_my_pkg开头的变量”),并且必须拒绝不安全的输入。
eval最常见的正确使用方式是从专门设计为以这种方式使用的程序输出中读取变量。例如,
# 在旧系统上,调整窗口大小后必须运行以下命令:
eval "`resize`"
# 更高级的用法:获取SSH私钥的密码短语。
# 这通常从.xsession或.profile类型的文件执行。
# ssh-agent生成的变量将被导出到用户会话中的所有进程,以便之后的ssh命令可以继承这些变量。
eval "`ssh-agent -s`"
eval还有其他用途,特别是在创建变量时(参考indirect variable references ↗)。以下是一种解析不带参数的命令行选项的示例:
# POSIX
#
# 动态创建选项变量。尝试调用:
#
# sh -x example.sh --verbose --test --debug
for i; do
case $i in
--test|--verbose|--debug)
shift # 从命令行中移除选项
name=${i#--} # 删除选项前缀
eval "$name=\$name" # 创建*新*变量
;;
esac
done
echo "verbose: $verbose"
echo "test: $test"
echo "debug: $debug"
那么,为什么这个版本是可接受的呢?这是因为我们限制了eval命令的使用,只有在输入是一组有限的已知值之一时才会执行。因此,用户无法滥用它以导致任意命令执行——任何包含奇怪内容的输入都不会匹配三个预定的可能输入之一。
请注意,这仍然是不推荐的:这是一条很陡峭的道路,稍后的维护很容易将这段代码变成危险的内容。例如,你想要添加一个功能,允许传递一堆不同的--test-xyz选项。你将--test更改为--test-*,而不费力地检查脚本的其他部分的实现。你测试你的用例,一切正常。不幸的是,你刚刚引入了任意命令执行:
$ ./foo --test-'; ls -l /etc/passwd;x='
-rw-r--r-- 1 root root 943 2007-03-28 12:03 /etc/passwd
再次强调:允许eval命令在未经过滤的用户输入上使用会导致任意命令执行。
尽一切可能避免将数据传递给eval,即使你的代码似乎处理了所有边界情况。
如果你经过深思熟虑并向#bash寻求了替代方法,但没有找到任何方法,请跳到"Robust eval usage"部分。
使用declare能更好地完成这个任务吗?
for i in "$@"; do
case "$i" in
--test|--verbose|--debug)
shift # 从命令行中移除选项
name=${i#--} # 删除选项前缀
declare $name=Yes # 设置默认值
;;
--test=*|--verbose=*|--debug=*)
shift
name=${i#--}
value=${name#*=} # value是第一个单词后面的内容和=
name=${name%%=*} # 仅限于第一个单词(即使值中有另一个=)
declare $name="$value" # 创建*新*变量
;;
esac
done
请注意,--name用于默认值,--name=value是必需的格式。
以下是eval的一个良好使用示例,用于从专门设计为以这种方式使用的程序输出中读取变量:
# 在旧系统上,调整窗口大小后必须运行以下命令:
eval "`resize`"
# 更高级的用法:获取SSH私钥的密码短语。
# 这通常从.xsession或.profile类型的文件执行。
# ssh-agent生成的变量将被导出到用户会话中的所有进程,以便之后的ssh命令可以继承这些变量。
eval "`ssh-agent -s`"
eval还可以用于创建变量时,尤其是在创建间接变量引用时。下面是一个解析不带参数的命令行选项的示例:
# POSIX
#
# 动态创建选项变量。尝试调用:
#
# sh -x example.sh --verbose --test --debug
for i; do
case $i in
--test|--verbose|--debug)
shift # 从命令行中移除选项
name=${i#--} # 删除选项前缀
eval "$name=\$name" # 创建*新*变量
;;
esac
done
echo "verbose: $verbose"
echo "test: $test"
echo "debug: $debug"
尽管这个示例中的eval使用看起来安全,但仍然不推荐广泛使用eval命令,因为它需要非常小心的输入过滤和验证,以避免任意命令执行漏洞。尽量避免将数据传递给eval,并寻找替代方案,以增加脚本的安全性。
难道使用declare不能更好地解决这个问题吗?
for i in "$@"; do
case "$i" in
--test|--verbose|--debug)
shift # 从命令行中移除选项
name=${i#--} # 删除选项前缀
declare $name=Yes # 设置默认值
;;
--test=*|--verbose=*|--debug=*)
shift
name=${i#--}
value=${name#*=} # 值是等号后面的内容
name=${name%%=*} # 仅限于第一个单词的名称(即使值中还有另一个等号)
declare $name="$value" # 创建*新的*变量
;;
esac
done
请注意,默认情况下,--name和--name=value是必需的格式。
对于某些输入,declare确实可以更好地工作:
griffon:~$ name='foo=x;date;x'
griffon:~$ declare $name=Yes
griffon:~$ echo $foo
x;date;x=Yes
但它仍然会导致数组变量中的任意代码执行:
attoparsec:~$ echo $BASH_VERSION
4.2.24(1)-release
attoparsec:~$ danger='( $(printf "%s!\n" DANGER >&2) )'
attoparsec:~$ declare safe=${danger}
attoparsec:~$ declare -a unsafe
attoparsec:~$ declare unsafe=${danger}
DANGER!
这段代码展示了使用declare可能引发的安全问题。在某些情况下,使用declare可能会导致任意代码执行,从而产生潜在的安全漏洞。在这个例子中,变量的值包含了一个命令,当使用declare声明变量时,该命令将被执行。这可能导致不受信任的代码执行,从而引发安全问题。
为了确保脚本的安全性,应该避免将不受信任的数据传递给declare命令。如果需要动态创建变量,可以考虑使用其他安全的方法或寻找替代方案,以避免潜在的安全风险。
几乎总是(至少在Bash中99%或更多的时间内,但也适用于更简洁的shell),正确地使用eval的方式是在库代码中生成隐藏在函数背后的抽象层。这允许函数具有以下功能:
通常,当满足以下至少全部条件时,eval是正确的:
如果出于某种原因仍然需要动态构建Bash代码并评估它,请确保采取以下预防措施:
为什么要注意?如果未能遵循上述建议,以下是脚本可能会受到利用的示例:
name='Bob; echo I am arbitrary code'; eval "user=$name"
echo 'echo I am arbitrary code' > /usr/local/bin/a[1]=b; chmod +x /usr/local/bin/a[1]=b; var='a[1]' value=b; eval "$(printf '%q=%q' "$var" "$value")"