该站点上的许多答案和评论都提到,通常应该避免eval
在 shell 脚本中使用。这些评论通常是针对初学者的。
我见过提到安全问题和稳健性,但例子很少。
我已经明白了eval
命令是什么是以及如何使用它。然而,那里给出的答案只是简单地提到了使用eval
.值得注意的是,每一个答案指出这eval
可能是一个主要的安全漏洞,但大多数人只是将这一事实作为结论性句子提及,而没有详细说明。这似乎值得进一步考虑,而且确实是它自己的问题。
为什么是使用eval
一个坏主意?
什么时候会用起来合适吗?
是否有任何指导原则可以完全安全地使用它,或者它总是不可避免地存在安全漏洞?
编辑:本文是我发布此问题时实际上正在寻找的规范参考(答案)。任何滥用者eval
都可以指出此参考。
答案1
这个问题会引起意见...
eval
是标准 shell 的一部分:
实用
eval
程序应通过将参数连接在一起来构造命令,并用一个分隔符分隔每个参数<space>
特点。构造的命令应由 shell 读取并执行。
不喜欢的可能原因eval
:
- 脚本编写者可能无法正确引用,从而导致意外行为(考虑来自评估中的变量的不匹配引用的可能性)。
- 评估的结果可能并不明显(部分是由于引用问题,部分是因为
eval
允许您根据其他变量的名称设置变量)。
另一方面,如果eval
检查输入的数据,例如测试文件名的结果并确保没有引号使事情变得复杂,那么它是一个有用的工具。通常建议的替代方案可移植性较差,例如,特定于某些特定的 shell 实现。
进一步阅读(但请记住 bash 提供了特定于实现/非标准的替代方案):
- 为什么在 Bash 中应该避免 eval,我应该使用什么来代替?
- bash 中的“eval”命令是什么?
- Unix/Linux Bash:发现关键安全漏洞
- 编写更好的 Shell 脚本 - 第 3 部分(参见 eval 示例)
- Shell 脚本安全(参见 eval 示例)
答案2
问题
什么时候使用比较合适?
当 eval 的参数被正确引用(两级)时,第一次解析中 eval 扩展的变量值将不会成为第二次解析中执行的命令。
为什么使用 eval 是一个坏主意?
如果您真的知道自己在做什么,那么这不是IMO(这个问题将征求意见)。
eval
第一:通过替换为和来检查您所写的(评估)是否符合您的预期。echo
第二:认真考虑哪些变量是本地设置的以及它们可能具有什么值。
是否有任何指导原则可以完全安全地使用它,或者它总是不可避免地存在安全漏洞?
是的,在某些情况下它可以完全安全地使用。
但这始终是一种风险(就像编写不正确的脚本存在风险一样)。
碰巧 eval 会将风险提高到某个指数。
这肯定需要一个很长的描述:
细节:
命令 eval (始终是内置命令)允许解析命令行两次。
rm -rf /
原则上,它并不比任何其他命令(想想)危险更多或更少。它将使用当前用户权限执行,因此在 root 身份下使用它之前请三思。但上面显示的命令也是如此rm
。对于有限用户,rm 在 root 拥有的大多数目录上都会失败。但这rm
仍然是一个危险的命令。
一个众所周知的成语。
命令 eval 在已知的习惯用法中非常有用(而且非常安全)。
例如,该表达式打印最后一个位置参数的值:
$ set -- "ls -l" "*" "c;date"
$ eval echo \"\$\{$#\}\" ### command under test
c;date
该命令是无条件安全的(事实上),因为唯一可能的结果$#
是数字。唯一可能的结果$n
(n 是数字)是该位置变量的内容。
如果您想查看该命令的作用,请将 eval 替换为 echo
$ set -- "ls -l" "*" "c; date"
$ echo echo \"\$\{$#\}\" ### command under test
echo "${3}"
并且echo ${3}
是非常常见(且安全)的习语。
我们仍然可以更改一些引用并得到相同的结果:
$ echo echo '"${'$#\}\"
echo "${3}"
$ eval echo '"${'$#\}\"
c;date
我希望您很容易看出这种新的引用方式比上面的方式更加晦涩难懂。
略有不同的习语。
$ b=book
$ book="A Tale of Two Cities"
$ eval 'a=$'"$b" ### safe for some values of $b.
$ echo "$a"
A Tale of Two Cities
在这里我们发现了 eval 的第一个(两个)也是主要问题:
引用不足:
$ b='book;date'
$ eval 'a=$'"$b" ### safe for some values of $b.
Fri Apr 22 22:03:09 UTC 2016
命令日期已执行(我们不打算这样做)。
但这不会执行date
(或者我想谢谢@Wildcard)。
该命令的正确解决方案是清理输入,但我将推迟讨论这个问题,直到定义了 eval 的第二个问题。
$ eval 'a="$'"$b"\"
$ echo "$a"
A Tale of Two Cities;date
而且技巧并不难,只需用 echo 替换 eval 并评估打印的命令行是否安全即可。比较不安全和安全命令:
$ echo 'a=$'"$b"
a=$book;date
$ echo 'a="$'"$b"\"
a="$book;date"
这些简单的双引号会将字符串保留为字符串,并且不会转换为由 shell 执行的命令。
如果我们放置“外部引号”和“内部引号”(为了易读而添加空格),可能更容易理解引用的逻辑:
$ echo a\=\"\$ "$b" \"
内部引用总是一个$b
“好主意”。
外部的都是反斜杠中的那些(在本例中)。
不带空格的命令(应该使用):
$ echo a\=\"\$"$b"\"
a="$book;date"
$ eval a\=\"\$"$b"\"
$ echo "$a"
A Tale of Two Cities;date
但即使这个例子也很快被用户打破(感谢@Wildcard):
$ b='";date;:"'
$ eval a\=\"\$"$b"\"
Fri Apr 22 23:25:43 UTC 2016
eval 执行的命令被阻止成为恶意命令,让我们使用 echo:
$ echo a\=\"\$"$b"\"
a="$";date;:""
思考两次引用的结果是什么绝非易事。
第二个问题。
这比第一个更复杂。在某些情况下,我们不希望 eval 参数的第一次扩展的结果保留引用。在很多情况下,因为我们想要在变量内执行命令。
或者,攻击者(如上所示)制作一个与引号匹配的字符串,然后将命令放在引号之外。该命令将被执行,并且(如果幸运的话)将报告错误。
这打破了第一层保护:引用
并使变量的值完全开放于执行。
在这种情况下,我们必须确定变量将具有的值。所有可能的值。
如果变量值由某个用户控制,我们会告诉该用户:
给我任何命令,我都会以我的权限执行它。
这始终是一个危险的赌注,一个肯定的错误,而且从根本上来说:这是一个疯狂的行为。
清理外部数据。
综上所述,这是完全安全的:
#!/bin/bash
a=${1//[^0-9]} ### Only leave a number (one or many digits).
eval echo $(( a + 1 ))
无论外部用户在位置参数中放置什么,它都会变成一个数字,它可能是一个很大的数字, $(( ... )) 将失败,但该输入不会触发任何命令的执行。
现在我们可以讨论b
上面命令中的清理变量了。命令是:
$ b='";date;:"'
$ eval a\=\"\$"$b"\"
Sat Apr 23 01:56:30 UTC 2016
因为b包含了几个用来换行的字符。
$ echo a\=\"\$"$b"\"
a="$";date;:""
更改为单引号不起作用,b 的其他一些值也可以匹配它们。我们需要b
通过删除(至少)引号来“清理”。
我们可以替换$b
为${b//\"}
:
$ eval a\=\"\$"${b//\"}"\"
$ echo "$a"
$;date;:
更强大的变量名称清理。
但是我们可以通过承认 shell 中的变量名只能包含0-9a-zA_Z
和下划线来使其变得更好。我们可以用这个删除所有其他的:
$ c="$( LC_COLLATE=C; echo "${b//[^0-9A-Za-z_]}" )"
这会将变量清理b
为有效的变量名称。
然后,我们可以通过实际检查内部变量名称$b
(使用 clean c
)是否存在来添加更严格的检查:
$ if declare -p "$c" &>/dev/null; then eval a\=\"\$"$c"\" ; fi
答案3
Eval 是针对不良代码的简单解决方案。这是我想到的一个例子:
假设您想通过引用获取数组元素。您可以使用 eval:
$ nov=(osc pap que)
$ rom=nov
$ eval echo \${$rom[2]}
que
更好的方法是:
$ nov=(osc pap que)
$ rom=nov[2]
$ echo ${!rom}
que
更好的方法仍然是:
$ tail -1 <<+
> osc
> pap
> que
> +
que