如何在运行时求值Erlang的代码字符串

Erlang有一种能力,在运行时读取一个代表一行代码的字符然后执行这行代码。

它可以解析这个字符串,执行它,然后返回结果。

  1. 求值简单的表达式
  2. 安全注意事项
    2.1. SQL注入攻击是什么?(请耐心看看)
    2.2 与Erlang有关的安全问题是什么?!
  3. 拦截本地函数调用
  4. 拦截非本地函数调用
  5. 还有什么?

求值简单的表达式

最基本的,我们可以读取任何传入的表达式并执行它。

1
2
3
4
5
6
7
8
9
10
11
12
-module(parser).

-export([
evaluate_expression/1
]).

-spec evaluate_expression(string) -> any().
evaluate_expression(Expression) ->
{ok, Tokens, _} = erl_scan:string(Expression), % 扫描代码为一些字符串标记
{ok, Parsed} = erl_parse:parse_exprs(Tokens), % 解析这些字符串标记为一个抽象格式
{value, Result, _} = erl_eval:exprs(Parsed, []), % 求值表达式,返回结果值
Result.

尝试传递一些简单的算术表达式。很简单吧?

记住,语句用逗号结束,而函数用句号结束,所以你需要包括那些标点符号。

1
2
3
4
5
6
7
8
9
> c(parser).
{ok,parser}

> parser:evaluate_expression("4 > 2.").
true
> parser:evaluate_expression("4+2.").
6
> parser:evaluate_expression("A=7+2,A-4.").
5

如果这就是你所需要的,真棒。

如果你有时间,你可能想要了解这开辟了潜在安全闸门。

安全注意事项

如果你只允许任何人执行任意一行代码,将会有一些很严重的问题,而且立刻就有更多的漏洞出现。

首先,要做一个比较…

SQL注入攻击是什么?(请耐心看看)

我要转换一下话题,谈谈SQL注入攻击。

例如,我们已经一个web页面,用户可以在这个页面上只输入他们的用户名来查看关于他们自己的信息。在幕后,我们只是获取用户输入的用户名,然后将其插入一个查询语句中,如下所示:

1
"select * from user_table where user_name = " + "gwinney"

只要用户规规矩矩地玩,一切都好。但是,如果他们输入的名字是 “gwinney; delete * from user_table” 呢?现在,这个查询语句变成如下所示:

1
"select * from user_table where user_name = " + "gwinney; delete * from user_table"

这个问题的解决方案(如果你把这样的代码提交到StackOverflow,至少一半的人会对你大吼大叫)是净化输入。我们应该检查一下以确保它做的是我们期望的,对于SQL来说,这通常意味着参数化查询。

我不想在此对这个问题进行太多的细述,但如果我们以正确的方式做的话,查询看起来更像下面这样,它这将查询失败,因为疯狂的用户名不存在。

1
"select * from user_table where user_name 'gwinney; delete * from user_table'"

与Erlang有关的安全问题是什么?!

相似地,我们可能会碰到表达式代码的安全问题。

我们被允许包含任何函数 – 本地函数、BIF(Erlang的内建函数)和在你创建的其他模块里暴露出来的函数 – 的调用,并且它将解析它们然后尝试执行它们。

如果我们让上述函数能被外界访问,即使是间接的,而且输入没有经过净化,那么我们就是给别人直接调用各种他们没有业务调用函数的能力。危险啊!

那么我们如何防止这样的危险呢?

拦截本地函数调用

我们可以给 erl_eval:exprs 提供一个函数,所有本地函数的调用都被传给它,我们就可以在这个函数里做一些额外的动作。

本地函数是那些在相同的模块里,它们可以不需要指定模块名而被调用。(虽然一些BIF,比如list_to_binary,不需要指定模块名,这是因为它们被系统自动导入了 – 它们仍然被认为是非本地的。)

在下面的代码里有一些新元素。一个叫做handle_local_function的函数和一个叫做get_random_number的本地函数(感谢xkcd)。这个处理函数输出一个相关信息然后处理传入给它的函数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-module(parser).

-export([
evaluate_expression/1
]).

-spec evaluate_expression(string) -> any().
evaluate_expression(Expression) ->
{ok, Tokens, _} = erl_scan:string(Expression),
{ok, Parsed} = erl_parse:parse_exprs(Tokens),
{value, Result, _} = erl_eval:exprs(Parsed, [],
{value, fun handle_local_function/2}),
Result.

-spec handle_local_function(atom(), list()) -> any().
handle_local_function(FunctionName, Arguments) ->
io:format("Local call to ~p with ~p~n", [FunctionName, Arguments]),
case FunctionName of
get_random_number -> get_random_number();
what_time_is_it -> calendar:universal_time();
are_we_there_yet -> "no";
_ -> "uh uh uh. you didn't say the magic word!"
end.

-spec get_random_number() -> integer().
get_random_number() ->
4. % 通过公平的骰子选择;保证是随机的

现在再运行这个模块,传入一些新的表达式。

我们可以拦截这些本地函数(可能不是真的存在,但表达式求值器不知道),并且按照我们的要求重定向它们…或者如果用户试图做一些无效的事情,只会吐出一条消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> c(parser).
{ok,parser}

> parser:evaluate_expression("get_random_number().").
Local call to get_random_number with []
4

> parser:evaluate_expression("what_time_is_it().").
Local call to what_time_is_it with []
{{2017,3,5},{15,21,53}}

> parser:evaluate_expression("are_we_there_yet().").
Local call to are_we_there_yet with []
"no"

parser:evaluate_expression("break_the_system().").
Local call to break_the_system with []
"uh uh uh. you didn't say the magic word!"

拦截非本地函数调用

相似地,我们可以给 erl_eval:exprs 提供一个函数,所有非本地函数的调用都被传给它。(当前模块外的任何东西,包括BIF、甚至用于比较的运算符。)

如下代码被扩展来处理非本地函数。注意,我们如何必须显式处理属于Erlang模块的一部分的 > 和 < 比较运算符,我们如何能够把不存在的函数重导向到存在的函数,以及如果一个函数不被支持,我们如何能够输出一条消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
-module(parser).

-export([
evaluate_expression/1
]).

-spec evaluate_expression(string) -> any().
evaluate_expression(Expression) ->
{ok, Tokens, _} = erl_scan:string(Expression),
{ok, Parsed} = erl_parse:parse_exprs(Tokens),
{value, Result, _} = erl_eval:exprs(Parsed, [],
{value, fun handle_local_function/2},
{value, fun handle_non_local_function/2}),
Result.

-spec handle_local_function(atom(), list()) -> any().
handle_local_function(FunctionName, Arguments) ->
io:format("Local call to ~p with ~p~n", [FunctionName, Arguments]),
case FunctionName of
get_random_number -> get_random_number();
what_time_is_it -> calendar:universal_time();
are_we_there_yet -> "no";
_ -> "uh uh uh. you didn't say the magic word!"
end.

-spec handle_non_local_function(atom(), list()) -> any().
handle_non_local_function({ModuleName,FunctionName}, Arguments) ->
io:format("Non-local call to ~p with ~p~n", [FunctionName, Arguments]),
case ModuleName of
erlang ->
case FunctionName of
'>' -> apply(ModuleName, FunctionName, Arguments);
'<' -> apply(ModuleName, FunctionName, Arguments);
list_to_binary -> apply(ModuleName, FunctionName, Arguments);
_ -> "nope"
end;
calendar ->
case FunctionName of
universal_time -> calendar:universal_time();
lets_pretend_this_returns_four -> 4;
something_ridiculous -> "what calendar are you using??";
_ -> "notgonnahappen"
end;
_ -> "don't think about it"
end.

-spec get_random_number() -> integer().
get_random_number() ->
4. % chosen by fair dice roll; guaranteed to be random

大于和小于比较是允许的,但是等于却不允许。一些函数被允许,一些函数不被允许,一些函数被重定向。在上一个例子中,一个邪恶的用户尝试用他们邪恶的计划搞垮系统。但他被挫败了。:p

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
> c(parser).
{ok,parser}

> parser:evaluate_expression("4 < 2.").
Non-local call to '<' with [4,2]
false

> parser:evaluate_expression("4 > 2.").
Non-local call to '>' with [4,2]
true

> parser:evaluate_expression("4 == 2.").
Non-local call to '==' with [4,2]
"nope"

> parser:evaluate_expression("list_to_binary(\"hi\").").
Non-local call to list_to_binary with ["hi"]
<<"hi">>

> parser:evaluate_expression("binary_to_list(<<\"hi\">>).").
Non-local call to binary_to_list with [<<"hi">>]
"nope"

> parser:evaluate_expression("calendar:universal_time().").
Non-local call to universal_time with []
{{2017,3,5},{21,4,42}}

> parser:evaluate_expression("calendar:local_time().").
Non-local call to local_time with []
"notgonnahappen"

> parser:evaluate_expression("calendar:lets_pretend_this_returns_four().").
Non-local call to lets_pretend_this_returns_four with []
4

> parser:evaluate_expression("calendar:something_ridiculous().").
Non-local call to something_ridiculous with []
"what calendar are you using??"

> parser:evaluate_expression("sys:terminate(some_process, \"buahaha\").").
Non-local call to terminate with [some_process,"buahaha"]
"don't think about it"

还有什么?

Erlang里的好例子很难得到,你在这里看到的是一些试验和错误。如果你发现自己试图解析代码,并在运行时执行它,也许这会让你理解得更深。

其他资源:

原文链接: https://grantwinney.com/how-to-evaluate-a-string-of-code-in-erlang-at-runtime/