为什么在Bash中应避免评估,我应该使用什么替代?


107

一次又一次,我看到Bash在Stack Overflow上使用eval了答案,而答案又被猛烈抨击了,旨在使用这样的“邪恶”构造。为什么eval这么邪恶?

如果eval不能安全使用,我应该怎么用呢?

Answers:


148

这个问题比眼神还重要。我们将从显而易见的内容开始: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.

这应该工作。只要$1in println从不脏,它也是安全的。

现在稍等片刻:我一直使用与我们最初一直使用的相同的未引用语法sudo!为什么在这里而不是在这里工作?为什么我们必须将所有内容都单引号? sudo有点现代:它知道将收到的每个参数都用引号引起来,尽管这过于简单了。 eval简单地将所有内容连接在一起。

不幸的是,没有内置的替代方法eval可以像实参一样处理参数sudoeval内置的shell 也不例外。这很重要,因为它在执行时会考虑周围代码的环境和范围,而不是像函数那样创建新的堆栈和范围。

评估替代

特定的用例通常可以替代eval。这是一个方便的清单。 command代表您通常发送给的内容eval;随便你。

无操作

一个简单的冒号在bash中是不可操作的:

:

创建一个子外壳

( 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"`"
# ...

4
我没有提到eval "export $var='$val'"...(?)
Zrin

1
@Zrin可能没有达到您的期望。 export "$var"="$val"可能就是您想要的。您可能唯一一次使用if的表单是var='$var2'您想要对它进行双重引用的,但是您不应该尝试在bash中执行类似的操作。如果确实需要,可以使用export "${!var}"="$val"
Zenexer

1
@anishsane:假设,x="echo hello world";然后执行所包含的内容x,我们可以使用eval $x但是,这$($x)是错误的,不是吗? 是的:这$($x)是错误的,因为它会先运行echo hello world然后尝试运行捕获的输出(至少在我认为您正在使用它的上下文中),除非您有名为“ hello踢踢” 的程序,否则它将失败。
乔纳森·勒夫勒

1
@tmow啊,所以您实际上想要评估功能。如果那是您想要的,则可以使用eval; 请记住,它有很多安全警告。这也表明您的应用程序存在设计缺陷。
Zenexer

1
ref="${REF}_2" echo "${!ref}"该示例是错误的,因为bash 执行命令之前会替换变量,所以它将无法按预期工作。如果ref之前确实未定义变量,则替换的结果为ref="VAR_2" echo "",这将被执行。
Yoory N.

17

如何确保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 不是邪恶的-只是被误解了:)


“ token_quote”函数如何使用其参数?我找不到有关此功能的任何文档...
Akito


我想我的措词不太清楚。我的意思是函数参数。为什么没有arg="$1"?for循环如何知道将哪些参数传递给函数?
Akito

除了简单的“误解”,我走得更远,它也经常被滥用并且确实不需要。Zenexer的答案涵盖了很多这样的情况,但是任何使用都eval应该是一个危险信号,并经过仔细检查以确认该语言确实没有提供更好的选择。
dimo414
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.