JavaScript 为啥不要使用 eval

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 为啥不要使用 eval相关的知识,希望对你有一定的参考价值。

作者:王欣彤
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

不推荐使用eval的原因有很多,
1、eval 太神秘了,以至于很多人用错。所以不推荐使用。
这并不是eval 不好而是因为容易被用错。这并不是eval 不好而是因为容易被用错。

eval只是一个普通的函数,只不过他有一个快速通道通向编译器,可以将string变成可执行的代码。有类似功能的还有Function ,
setInterval 和 setTimeout。
2、 eval不容易调试。用chromeDev等调试工具无法打断点调试,所以麻烦的东西也是不推荐使用的…
3、说到性能问题,在旧的浏览器中如果你使用了eval,性能会下降10倍。在现代浏览器中有两种编译模式:fast path和slow path。fast path是编译那些稳定和可预测(stable and predictable)的代码。而明显的,eval不可预测,所以将会使用slow path ,所以会慢。
还有一个是,在使用类似于Closure Compiler等压缩(混淆)代码时,使用eval会报错。
(又慢又报错,我还推荐吗?)
4、关于安全性,我们经常听到eval是魔鬼,他会引起XSS攻击,实际上,如果我们对信息源有足够的把握时,eval并不会引起很大的安全问题。而且不光是eval,其他方式也可能引起安全问题。比如:
莫名其妙给你注入一个<script src="">标签,或者一段来历不明的JSON-P请求,再或者就是Ajax请求中的eval代码…
所以啊,只要你的信息源不安全,你的代码就不安全。不单单是因为eval引起的。
你用eval的时候会在意XSS的问题,你越在意就越出问题,出的多了,eval就成噩梦了。
5、效率问题是程序逻辑问题。对于一些有执行字符串代码需求的程序中,不用eval而用其他方式模拟反而会带来更大的开销。
参考技术A 破坏代码可阅读性

eval(string)

参数
描述

string 必需。要计算的字符串,其中含有要计算的 javascript 表达式或要执行的语句。
简单点就是说eval方法接收任何字符串,然后会编译和运行字符串,把它当成js代码来执行,到这里,我就发现一点不好的地方了。
如果我们要执行一段代码,一行一样写下来多好,为什么要把这些代码放到eval方法里来执行呢?
这无疑会破话代码的可阅读行,还有增加调试的难度。
eval伪装
有如下代码:

var foo = 1;
function test()
var foo = 2;
eval('foo = 3');
return foo;

test(); // 3
foo; // 1

但是只有当eval被直接调用时才会按预期执行,否则eval将在全局作用域下执行。
var foo = 1;
function test()
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;

test(); // 2
foo; // 3

所以,当对eval使用的位置不恰当是,会得到不同的结果,这样既混淆了结果,也不利于程序的执行,例如js的settimeout函数的第一个参数也接收字符串形式的方法,但是必须为全局定义的函数,内部定义的则不起作用。
function a()
console.log(1);

function b()
var a = function()
console.log(3);

setTimeout('a();',1000);

b();//1

执行b方法打印出1而不是3则充分证明这一点。
安全性太差
由于eval方法接收任何字符串形式的参数,那么就会对安全性进行破坏,例如下面这段代码:
var c = $('input').val();
var obj = eval('(username:'+c+')');

如果我在文本框输入alert(1)会得到什么结果呢?,可想而知,跟sql植入一个道理会破话安全性。
和JSON.parse比较
单纯比较:
var json_str = "\"username\":\"dd\"";
JSON.parse(json_str);//Object username: "dd"
eval('('+json_str+')'); //Object username: "dd"

看起来很正常,但是如果这样:

var json_str = "\"username\":1+1"
JSON.parse(json_str);//SyntaxError: Unexpected token +
eval('('+json_str+')');//Object username: 2

可以看出json.parse会首先检查语法是否正确,而eval则不会检查,无条件执行,这样话还是用json.parse好一些。

为啥要在 Bash 中避免使用 eval,而应该使用啥?

【中文标题】为啥要在 Bash 中避免使用 eval,而应该使用啥?【英文标题】:Why should eval be avoided in Bash, and what should I use instead?为什么要在 Bash 中避免使用 eval,而应该使用什么? 【发布时间】:2013-07-05 22:30:30 【问题描述】:

我一次又一次地在 Stack Overflow 上看到使用 eval 的 Bash 答案,并且由于使用这种“邪恶”构造,答案被抨击,双关语。为什么eval这么邪恶?

如果eval不能安全使用,我应该改用什么?

【问题讨论】:

不只是eval 可能不好;有关其他示例,请参阅vidarholen.net/contents/blog/?p=716。有许多 shell 构造最终会被评估、扩展等,而将 only 集中在 eval 上是不好的,因为它会产生错误的安全感。了解使用任何不受信任的数据的风​​险以及如何利用这些数据非常重要。也就是说,一个好的 SO 答案应该假设不受信任的外部数据或至少警告可能存在的陷阱,所以我倾向于同意抨击,除非eval 被大量不公平地挑选。 @ThomasGuyot-Sionnest 我认为我在回答中很好地涵盖了这一点。请注意,这是一个自我回答的问题;我故意问了一个我从其他人那里听到很多的问题,尽管我已经知道答案了。 【参考方案1】:

这个问题比表面上看到的要复杂得多。我们将从显而易见的开始: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.

这应该可行。只要println 中的$1 不脏,它也是安全的。

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

不幸的是,没有像sudo 那样处理参数的eval 的替代替代品,因为eval 是内置的shell;这很重要,因为它在执行时会占用周围代码的环境和范围,而不是像函数那样创建新的堆栈和范围。

评估替代方案

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

无操作

简单的冒号在 bash 中是无操作的:

:

创建子shell

( 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.

基于变量的重定向

在调用代码中,将&amp;3(或任何高于&amp;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

在被调用的函数内,重定向到&amp;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"`"
# ...

【讨论】:

我没有提到eval "export $var='$val'" ... (?) @Zrin 很有可能这并没有达到您的预期。 export "$var"="$val" 可能是你想要的。您可能会使用您的表单的唯一时间是var='$var2',并且您想对它进行双重取消引用——但您不应该尝试在 bash 中做类似的事情。如果确实需要,可以使用export "$!var"="$val" @anishsane: 对于你的假设,x="echo hello world"; 然后执行x 中包含的任何内容,我们可以使用eval $x 但是,$($x) 是错误的,不是吗? 是的:$($x) 是错误的,因为它运行 echo hello world 然后尝试运行捕获的输出(至少在我认为您正在使用它的上下文中),除非您有一个名为hello 的程序正在运行。 @tmow 啊,所以你实际上想要 eval 功能。如果这就是你想要的,那么你可以使用 eval;请记住,它有很多安全警告。这也表明您的应用程序存在设计缺陷。 ref="$REF_2" echo "$!ref" 示例是错误的,它不会按预期工作,因为 bash 在执行命令之前替换变量。如果ref变量之前真的是未定义的,那么替换的结果将是ref="VAR_2" echo "",这就是将被执行的内容。【参考方案2】:

如何使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 The in words part of a for loop is optional. 我想我措辞太不清楚了。我的意思是函数参数。为什么没有arg="$1"? for 循环如何知道哪些参数被传递给函数? 我会比简单地“被误解”更进一步,它也经常被误用并且真的不需要。 Zenexer 的回答涵盖了很多此类情况,但任何使用 eval 都应该是一个危险信号,并仔细检查以确认该语言确实没有提供更好的选择。

以上是关于JavaScript 为啥不要使用 eval的主要内容,如果未能解决你的问题,请参考以下文章

为啥 MongoDB 文档建议不要使用 DBREF?

为啥人们建议不要使用 Amazon RDS?

为啥这个使用Combiner 类的Hadoop 示例不能正常工作? (不要执行Combiner提供的“局部缩减”)

为啥 forEach 比常规迭代器更受欢迎?

为啥 auth0 建议不要在 localStorage 中存储令牌?

为啥 C# 中的 async/await 会返回可为空的值,即使被告知不要这样做?