内容

名称

安全 - 在受限隔间中编译和执行代码

概要

use Safe;

$compartment = new Safe;

$compartment->permit(qw(time sort :browse));

$result = $compartment->reval($unsafe_code);

描述

Safe 扩展模块允许创建隔间,在其中可以评估 Perl 代码。每个隔间都有

一个新的命名空间

命名空间的“根”(即“main::”)将更改为不同的包,在隔间中评估的代码无法引用此命名空间之外的变量,即使使用运行时 glob 查找和其他技巧。

在隔间之外编译的代码可以选择将变量放入(或共享变量)隔间的命名空间,只有这些数据对在隔间中评估的代码可见。

默认情况下,与隔间共享的唯一变量是“下划线”变量 $_ 和 @_(以及技术上不太常用的 %_,_ 文件句柄等)。这是因为否则默认使用 $_ 的 Perl 运算符将无法工作,并且在子例程进入时将参数分配给 @_ 也不起作用。

运算符掩码

每个隔间都有一个关联的“运算符掩码”。回想一下,Perl 代码在执行之前被编译成内部格式。评估 Perl 代码(例如通过“eval”或“do 'file'”)会导致代码被编译成内部格式,然后,如果编译没有错误,则执行。在隔间中评估的代码根据隔间的运算符掩码进行编译。尝试在包含掩码运算符的隔间中评估代码会导致编译失败并出现错误。代码将不会执行。

新创建隔间的默认运算符掩码是 ':default' optag。

重要的是,您需要阅读 Opcode 模块文档以获取更多信息,尤其是关于 opnames、optags 和 opsets 的详细定义。

由于只有在编译阶段运算符掩码才适用,因此可以通过将指向包装子例程(在隔间之外编写)的句柄放入隔间来实现对潜在不安全操作的受控访问。例如,

$cpt = new Safe;
sub wrapper {
  # vet arguments and perform potentially unsafe operations
}
$cpt->share('&wrapper');

警告

Safe 模块没有为使用 Perl 解释器评估不受信任的代码实现有效的沙箱。

Perl 解释器中可能被滥用来绕过 Safe 限制的错误不被视为漏洞。有关更多信息,请参阅 perlsecpolicy

作者对该软件是否适合安全目的不作任何明示或暗示的保证。

作者在任何情况下均不对因使用该软件而产生的特殊、偶然、后果性、间接或其他类似损害负责。

您的里程可能会有所不同。如有任何疑问,请勿使用它

方法

要创建一个新的隔间,请使用

$cpt = new Safe;

可选参数为 (NAMESPACE),其中 NAMESPACE 是用于隔室的根命名空间(默认为“Safe::Root0”,每个新隔室递增)。

请注意,Safe 模块的 1.00 版支持第二个可选参数 MASK。该功能已被撤回,待进一步考虑。请使用下面描述的 permit 和 deny 方法。

然后可以使用以下方法在由上述构造函数返回的隔室对象上使用。对象参数在每种情况下都是隐式的。

permit (OP, ...)

允许在隔室中编译代码时使用列出的运算符(除了任何已允许的运算符)。

您可以通过名称列出操作码,或使用标签名称;请参阅 "Opcode 中的预定义操作码标签"

permit_only (OP, ...)

允许在隔室中编译代码时使用列出的运算符(允许使用其他运算符)。

deny (OP, ...)

禁止在隔室中编译代码时使用列出的运算符(其他运算符可能仍然允许)。

deny_only (OP, ...)

禁止在隔室中编译代码时使用列出的运算符(所有其他运算符都将被允许,因此您可能不想使用此方法)。

trap (OP, ...), untrap (OP, ...)

trap 和 untrap 方法分别是 deny 和 permit 的同义词。

share (NAME, ...)

这将参数列表中的变量与隔室共享。这与使用 Exporter 模块导出变量几乎相同。

每个 NAME 必须是非词法变量的名称,通常包含前导类型标识符。一个裸字被视为函数名。

合法名称的示例包括标量的 '$foo'、数组的 '@foo'、哈希的 '%foo'、子例程的 '&foo' 或 'foo' 以及全局变量的 '*foo'(即与“foo”关联的所有符号表条目,包括标量、数组、哈希、子例程和文件句柄)。

假设每个 NAME 都在调用包中。请参阅 share_from 以获取替代方法(share 使用该方法)。

share_from (PACKAGE, ARRAYREF)

此方法类似于 share(),但允许您明确命名应从中共享符号的包。符号名称(包括类型字符)作为数组引用提供。

$safe->share_from('main', [ '$foo', '%bar', 'func' ]);

名称可以包含包名称,这些名称相对于指定的 PACKAGE。因此,这两个调用具有相同的效果

$safe->share_from('Scalar::Util', [ 'reftype' ]);
$safe->share_from('main', [ 'Scalar::Util::reftype' ]);

varglob (VARNAME)

这将返回隔室包中 VARNAME 的符号表条目的全局变量引用。VARNAME 必须是变量的名称,没有任何前导类型标记。例如

${$cpt->varglob('foo')} = "Hello world";

$cpt = new Safe 'Root';
$Root::foo = "Hello world";

效果相同,但避免了需要知道 $cpt 的包名。

reval (STRING, STRICT)

这将 STRING 作为 perl 代码在隔间内进行评估。

代码只能看到隔间的命名空间(由 root 方法返回)。隔间的根包对隔间内的代码来说是 main:: 包。

STRING 中的代码尝试使用隔间不允许的运算符将导致错误(在主程序运行时,但在 STRING 中的代码编译时)。错误的形式为 "'%s' trapped by operation mask..."。

如果以这种方式捕获了运算符,则 STRING 中的代码将不会执行。如果发生这种捕获的运算符或任何其他编译时或返回错误,则 $@ 将设置为错误消息,就像 eval() 一样。

如果没有错误,则该方法将返回最后一个表达式评估的值,或者可以使用 return 语句,就像子程序和 eval() 一样。上下文(列表或标量)由调用者按通常方式确定。

如果 reval() 的返回值是(或包含)任何代码引用,则这些代码引用将被包装,以便它们本身始终在隔间中执行。参见 "wrap_code_refs_within"

以前未记录的 STRICT 参数设置严格性:如果为真,则使用 'use strict;',否则使用 'no strict;'。注意:如果省略 STRICT,则 'no strict;' 是默认值。

一些需要注意的要点

如果允许 entereval 操作,则代码可以使用 eval "..." 来“隐藏”可能使用被拒绝操作的代码。这不是一个主要问题,因为当代码尝试执行 eval 时,它将失败,因为操作掩码仍然有效。但是,这种技术将允许聪明且可能有害的代码“探测”可能的边界。

在隔间中执行的代码或从隔间中执行的代码调用的代码执行的任何字符串 eval 将在隔间的命名空间中进行 eval。这是一个潜在的严重问题。

考虑一个在包 pkg 中的函数 foo(),它是在隔间之外编译的,但与它共享。假设隔间有一个名为 'Root' 的根包。如果 foo() 包含一个像 eval '$foo = 1' 这样的 eval 语句,那么通常情况下,$pkg::foo 将被设置为 1。如果 foo() 从隔间(通过任何方式)调用,那么它不会设置 $pkg::foo,而是 eval 实际上会设置 $Root::pkg::foo。

这可以通过使用一个模块(例如 Socket 模块)来轻松地演示,该模块使用 eval "..." 作为 AUTOLOAD 函数的一部分。你可以在隔间之外“使用”该模块,并与隔间共享一个(自动加载的)函数。如果在隔间中的代码或从隔间中以任何方式调用的任何代码触发了自动加载,那么 Socket 模块的 AUTOLOAD 函数中的 eval 将在隔间的命名空间中发生。由 eval'd 代码创建或使用的任何变量现在都受隔间中代码的控制。

类似的效果适用于从隔间调用但未在其中编译的代码中的所有运行时符号查找。

rdo (FILENAME)

这将在隔间内评估文件 FILENAME 的内容。它使用与 Perl 内置的 do 相同的规则来定位文件,可能使用 @INC

有关更多详细信息,请参见上面有关 reval 方法的文档。

root (NAMESPACE)

此方法返回隔间命名空间根部的包的名称。

请注意,此行为与 Safe 模块的 1.00 版本不同,在该版本中,根模块可用于更改命名空间。该功能已被撤回,等待更深入的考虑。

mask (MASK)

这是隔间运算符掩码的获取或设置方法。

如果没有 MASK 参数,它将返回隔间的当前运算符掩码。

如果存在 MASK 参数,它将为隔间设置运算符掩码(等效于调用 deny_only 方法)。

wrap_code_ref (CODEREF)

返回对匿名子例程的引用,该子例程在执行时将调用 CODEREF,并使 Safe 隔间“生效”。换句话说,调整包命名空间并启用 opmask。

请注意,opmask 不会影响已编译的代码,它只会影响已编译代码可能尝试执行的任何进一步编译。

当应用于从 reval() 返回的代码引用时,这特别有用。

(它还提供了一种针对 RT#60374 的解决方法:“Safe.pm sort {} bug with -Dusethreads”。有关更多详细信息,请参见 https://rt.perl.org/rt3//Public/Bug/Display.html?id=60374。)

wrap_code_refs_within (...)

将参数中找到的任何 CODE 引用包装起来,方法是将每个引用替换为对 "wrap_code_ref" 的调用结果,该调用作用于 CODE 引用。参数中的任何 ARRAY 或 HASH 引用都会递归地进行检查。

不返回任何内容。

风险

本节仅概述了隔间中的代码可能执行的操作(有意或无意),这些操作可能会对隔间外部产生影响。

内存

消耗所有(或几乎所有)可用内存。

CPU

导致无限循环等。

窥探

将私有信息复制到您的系统之外。即使像用户名这样简单的东西对其他人来说也是有价值的。例如,您可以从环境变量中获取许多有用的信息。

信号

导致信号(尤其是 SIGFPE 和 SIGALARM)影响您的进程。

设置信号处理程序需要仔细考虑和控制。当调用信号处理程序时,生效的是哪个掩码?如果用户可以使导入的函数获得异常并调用用户的信号处理程序,那么用户的受限掩码是否会在调用处理程序之前恢复?导入的处理程序是使用其原始掩码还是用户的掩码被调用?

状态更改

诸如 chdir 之类的操作显然会影响整个进程,而不仅仅是隔间中的代码。诸如 rand 和 srand 之类的操作具有类似但更微妙的影响。

作者

最初由 Malcolm Beattie 设计和实现。

由 Tim Bunce 重构以使用 Opcode 模块并添加其他更改。

目前由 Perl 5 移植者维护,<[email protected]>。