我一次又一次地使用eval
在Stack Overflow上看到Bash的答案,并且对于使用这样一个“邪恶”的构造,答案会被打击,双关语。为什么eval
如此邪恶?
如果eval
无法安全使用,我应该使用什么呢?
答案 0 :(得分:131)
这个问题远远超出了眼睛。我们将从显而易见的开始:eval
有可能执行“脏”数据。脏数据是指未被重写为安全使用情况-XYZ的任何数据;在我们的例子中,它是任何未经过格式化的字符串,以便安全评估。
乍一看,清理数据似乎很容易。假设我们抛出了一个选项列表,bash已经提供了一种清理单个元素的好方法,以及另一种将整个数组清理为单个字符串的方法:
function println
{
# Send each element as a separate argument, starting with the second element.
# Arguments to printf:
# 1 -> "$1\n"
# 2 -> "$2"
# 3 -> "$3"
# 4 -> "$4"
# etc.
printf "$1\n" "${@:2}"
}
function error
{
# Send the first element as one argument, and the rest of the elements as a combined argument.
# Arguments to println:
# 1 -> '\e[31mError (%d): %s\e[m'
# 2 -> "$1"
# 3 -> "${*:2}"
println '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit "$1"
}
# This...
error 1234 Something went wrong.
# And this...
error 1234 'Something went wrong.'
# Result in the same output (as long as $IFS has not been modified).
现在说我们要添加一个选项,将输出重定向为println的参数。当然,我们可以在每次调用时重定向println的输出,但是为了示例,我们不打算这样做。我们需要使用eval
,因为变量不能用于重定向输出。
function println
{
eval printf "$2\n" "${@:3}" $1
}
function error
{
println '>&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit $1
}
error 1234 Something went wrong.
看起来不错吧?问题是,eval解析两次命令行(在任何shell中)。在解析第一遍引用时,删除了一层引用。删除引号后,会执行一些变量内容。
我们可以通过让变量扩展在eval
内进行修复。我们所要做的就是单引一切,将双引号留在原处。一个例外:我们必须在eval
之前扩展重定向,因此必须保持在引号之外:
function println
{
eval 'printf "$2\n" "${@:3}"' $1
}
function error
{
println '&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
exit $1
}
error 1234 Something went wrong.
这应该有效。只要$1
中的println
永远不会变脏,它也是安全的。
现在请稍等一下:我使用与sudo
原来一直使用的未加引号语法相同的语法!它为什么在那里工作,而不是在这里?为什么我们必须单引一切? sudo
有点现代:它知道用引号括起它接收到的每个参数,尽管这是一种过度简化。 eval
简单地连接所有内容。
不幸的是,eval
没有像sudo
那样处理参数的替代品,因为eval
是内置的shell;这很重要,因为它在执行时会占用周围代码的环境和范围,而不是像函数一样创建新的堆栈和范围。
特定用例通常具有eval
的可行替代方案。这是一个方便的清单。 command
代表您通常发送给eval
的内容;无论你喜欢什么,都可以替代。
bash中的no-op中的一个简单冒号: :
( command ) # Standard notation
永远不要依赖外部命令。你应该始终控制返回值。把这些放在他们自己的路上:
$(command) # Preferred
`command` # Old: should be avoided, and often considered deprecated
# Nesting:
$(command1 "$(command2)")
`command "\`command\`"` # Careful: \ only escapes $ and \ with old style, and
# special case \` results in nesting.
在调用代码时,将&3
(或任何高于&2
的内容)映射到目标:
exec 3<&0 # Redirect from stdin
exec 3>&1 # Redirect to stdout
exec 3>&2 # Redirect to stderr
exec 3> /dev/null # Don't save output anywhere
exec 3> file.txt # Redirect to file
exec 3> "$var" # Redirect to file stored in $var--only works for files!
exec 3<&0 4>&1 # Input and output!
如果是一次性通话,则不必重定向整个shell:
func arg1 arg2 3>&2
在被调用的函数中,重定向到&3
:
command <&3 # Redirect stdin
command >&3 # Redirect stdout
command 2>&3 # Redirect stderr
command &>&3 # Redirect stdout and stderr
command 2>&1 >&3 # idem, but for older bash versions
command >&3 2>&1 # Redirect stdout to &3, and stderr to stdout: order matters
command <&3 >&4 # Input and output!
情景:
VAR='1 2 3'
REF=VAR
为:
eval "echo \"\$$REF\""
为什么呢?如果REF包含双引号,则会破坏并打开代码以利用漏洞。可以对REF进行消毒,但是当你有这个时,这是浪费时间:
echo "${!REF}"
没错,bash在版本2中内置了变量间接。如果你想做一些更复杂的事情,它会比eval
更棘手:
# Add to scenario:
VAR_2='4 5 6'
# We could use:
local ref="${REF}_2"
echo "${!ref}"
# Versus the bash < 2 method, which might be simpler to those accustomed to eval:
eval "echo \"\$${REF}_2\""
无论如何,新方法更直观,但对于经验丰富的编程人员来说,这似乎不太适合eval
。
关联数组本质上是在bash 4中实现的。一个警告:它们必须使用declare
创建。
declare -A VAR # Local
declare -gA VAR # Global
# Use spaces between parentheses and contents; I've heard reports of subtle bugs
# on some versions when they are omitted having to do with spaces in keys.
declare -A VAR=( ['']='a' [0]='1' ['duck']='quack' )
VAR+=( ['alpha']='beta' [2]=3 ) # Combine arrays
VAR['cow']='moo' # Set a single element
unset VAR['cow'] # Unset a single element
unset VAR # Unset an entire array
unset VAR[@] # Unset an entire array
unset VAR[*] # Unset each element with a key corresponding to a file in the
# current directory; if * doesn't expand, unset the entire array
local KEYS=( "${!VAR[@]}" ) # Get all of the keys in VAR
在旧版本的bash中,您可以使用变量间接:
VAR=( ) # This will store our keys.
# Store a value with a simple key.
# You will need to declare it in a global scope to make it global prior to bash 4.
# In bash 4, use the -g option.
declare "VAR_$key"="$value"
VAR+="$key"
# Or, if your version is lacking +=
VAR=( "$VAR[@]" "$key" )
# Recover a simple value.
local var_key="VAR_$key" # The name of the variable that holds the value
local var_value="${!var_key}" # The actual value--requires bash 2
# For < bash 2, eval is required for this method. Safe as long as $key is not dirty.
local var_value="`eval echo -n \"\$$var_value\""
# If you don't need to enumerate the indices quickly, and you're on bash 2+, this
# can be cut down to one line per operation:
declare "VAR_$key"="$value" # Store
echo "`var_key="VAR_$key" echo -n "${!var_key}"`" # Retrieve
# If you're using more complex values, you'll need to hash your keys:
function mkkey
{
local key="`mkpasswd -5R0 "$1" 00000000`"
echo -n "${key##*$}"
}
local var_key="VAR_`mkkey "$key"`"
# ...
答案 1 :(得分:10)
eval
安全 eval
可以安全地使用-但必须首先引用其所有参数。方法如下:
此功能将为您完成:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
用法示例:
提供一些不受信任的用户输入:
% input="Trying to hack you; date"
构造一个命令进行评估:
% cmd=(echo "User gave:" "$input")
用正确的报价貌似对它进行评估:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
请注意,您被黑了。 date
被执行,而不是照字面意思打印。
用token_quote()
代替:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
不是邪恶的-只是被误解了:)
答案 2 :(得分:-4)
怎么样?
ls -la /path/to/foo | grep bar | bash
或
(ls -la /path/to/foo | grep bar) | bash