Lisp 将代码作为数据处理是否比不将代码作为数据处理的语言更容易受到安全漏洞的攻击?

Does Lisp's treatment of code as data make it more vulnerable to security exploits than a language that doesn't treat code as data?

我知道这可能是个愚蠢的问题,但我很好奇。
由于 Lisp 对待代码和数据的方式相同,这是否意味着编写有效载荷并将其作为可用于利用程序的“无害”数据传递更容易?与不这样做的语言相比?

例如在 python 你可以做这样的事情。

malicious_str = "print('this is a malicious string')"
user_in = eval(malicious_str)

>>> this is a malicious string

P.S刚开始学Lisp

一不小心,任何语言都可能被利用。

一个众所周知的针对 Lisp 的攻击是通过 #. reader 宏:

(read-from-string "#.(start-the-war)")

如果 *read-eval* is non-nil - this is why one should always bind it when read 来自不受信任的流,将启动 war。

然而,这与“代码即数据”学说没有直接关系...

不,我不这么认为。事实上,由于 Lisp 中 'code is data' 的通常含义,它可能 比其他一些语言更容易受到攻击。

[注意:这个答案实际上是关于 Common Lisp 的:请参阅末尾以获取相关说明。]

语言中'code can be data'有两种意义。

将对象变成可执行代码:eval & friends

这是第一感觉。这意味着你可以,比如说,拿一个字符串或一些其他对象(显然不是所有类型的对象)并说 'turn this into something I can execute, and do that'.

任何可以做到这一点的语言都具有

  • 非常小心地处理不受约束的数据;
  • 或者能够确定给定的程序实际上并没有这样做。

很多语言都有 eval 的等价物及其关系,所以很多语言都有这个问题。比如你举了一个Python的例子,很好,Python里面可能还有其他的例子(我在Python2里面写过程序,支持动态加载运行 时的模块,执行潜在的任意代码,我认为这些东西更好地集成在 Python 3).

这也不仅仅是一种语言的 属性:它是一种系统的 属性。 C不能这样做,对吧?嗯,是的,如果你在任何一种合理的 *nixy 平台上,它就可以。您不仅可以使用 exec 系列函数,还可以动态加载共享库并在其中执行代码。

所以这个问题的一个解决方案是以某种方式能够确定给定的程序不会这样做.有帮助的一件事是,如果有有限的、已知的方法可以做到这一点。在 Common Lisp 中,我认为这些可能是

  • eval当然;
  • 不受约束read(因为*read-eval*);
  • load;
  • compile;
  • compile-file;
  • 可能还有一些我忘记了。

嗯,你能在程序中静态地检测到对这些调用吗?不是真的:考虑一下:

(funcall (symbol-function (find-symbol s)) ...)

现在你有麻烦了,除非你能很好地控制 s 是什么:例如它可能是 "EVAL"

所以这很可怕,但我不认为它 比 Python 可以做的更可怕,例如(几乎可以肯定你可以在命名空间中四处闲逛)找到 eval 之类的东西?)。在程序中类似的东西应该是一个非常重要的暗示,表明可能会发生坏事。

我认为可能有两种方法,CL 都没有采用,但哪些实现可以(甚至用 CL 编写的程序也可以)。

  • 一个人能够运行 以这样一种方式编写程序,即上面的有限组错误函数根本不允许:如果您尝试调用它们,它们会发出错误信号。一个实现可以清楚地做到这一点(见下文)。
  • 另一个是像 Perl 的 'tainting' 那样的东西,其中来自用户的数据需要在使用之前以某种方式由程序明确查看。这当然不能保证安全,但确实更难犯愚蠢的错误:如果上面的 s 来自用户输入并因此被污染,你必须明确地说“可以使用它”并且,好吧,那就看你的了。

所以这是一个问题,但我认为它 并不比许多其他语言(和语言家族)所存在的问题更糟。

可以解决第一种方法的实现示例是 LispWorks:如果您使用 LW 构建应用程序,通常会使用名为 deliver 的函数创建二进制文件,该函数具有允许您从生成的二进制文件中删除函数定义的选项,无论交付过程是否会将它们留在那里。所以,例如

(deliver 'foo "x" 5
         :functions-to-remove '(eval load compile compile-file read))

将生成一个可执行文件 x,无论它做了什么,都无法调用这些函数,因为它们根本不存在。

其他实现可能具有类似的功能:我只是不了解它们。

但是在 Lisp 中 'code is data' 还有另一种意义。

程序源代码可作为结构化数据使用

这就是人们在 Lisp 中说 'code is data' 时真正的意思,即使他们不知道。值得再次查看您的 Python 示例:

>>> eval("exit('exploded')")
exploded
$

那么 eval 吃的是一个字符串:一个完全非结构化的字符向量。如果您想知道该字符串是否包含令人讨厌的内容,那么,您还有很多工作要做(免责声明:见下文)。

将此与 CL 进行比较:

> (let ((trying-to-be-bad "(end-the-world :now t)"))
    (eval trying-to-be-bad))
"(end-the-world :now t)"

好吧,那显然没有世界末日。它并没有结束世界,因为 eval 评估了一些 Lisp 源代码,而字符串的值,作为源代码,就是字符串。

如果我想做一些讨厌的事情,我必须给它一个真正有趣的结构:

> (let ((actually-bad '(eval (progn
                               (format *query-io* "? ")
                               (finish-output *query-io*)
                               (read *query-io*)))))
    (eval actually-bad))
? (defun foo () (foo))
foo

现在这至少在几个方面可能非常令人讨厌。但是等等:为了做这件讨厌的事,我不得不交出 eval 一大块源代码 表示为 s 表达式 。而且那个s-expression的结构是完全开放给我检查的。我可以编写一个程序,以我喜欢的任意方式检查这个 s 表达式,并决定它是否为我所接受。这比 'given this string, interpret it as a piece of source text for the language and tell me if it is dangerous':

简单多了
  • 将字符序列转换为s表达式的过程已经发生;
  • s表达式的结构既简单又标准。

因此,在 'code is data' 的这个意义上,Lisp 可能 更安全 比其他具有 eval 版本的语言更安全 ,例如 Python,比如说,因为代码是结构化标准简单数据。 Lisp 对可怕的 'language in a string' 问题有一个答案。


我相当确定 Python 实际上确实有一些方法可以使解析树以可检查的标准方式可用。但是eval还是很开心的吃串

正如我上面所说,这个答案是关于Common Lisp的。但是当然还有许多其他的 Lisp,它们会有不同版本的这个问题。 Racket 例如,使用沙盒执行和模块可能真的可以相当严格地限制事物,尽管我还没有探索过这个。