【问题标题】:How to safely return objects from an OS-level Python sandbox?如何从操作系统级别的 Python 沙箱安全地返回对象?
【发布时间】:2013-11-11 04:43:53
【问题描述】:

我需要能够运行不受信任的 Python 脚本。经过大量研究,似乎只有 Python 的沙箱并不安全,至少使用 CPython(我需要使用它)。

因此,我们也计划使用操作系统级别的沙盒(SELinux、AppArmor 等)。

我的问题是:我们如何安全地与沙盒通信?沙箱中的代码将需要返回 Python 类型,例如 int 和 str,以及 Numpy 数组。未来可能会有更多类型。

显而易见的方法是使用 pickle,但沙箱中的一些恶意代码似乎可能会获取输出管道(我们正在考虑使用 0MQ)并发送回一些可能导致任意代码执行的东西在沙盒外解封。

是否有没有 JSON 等性能开销的更安全的序列化替代方案来替代 pickle?

我们使用的是 Python 3.3。

【问题讨论】:

  • 从使用包含 100 个元素的 100K 列表的简单 dict 进行的快速测试中,pickle.dumps 耗时 35.6 毫秒,json.dumps 耗时 71.6 毫秒。这是您担心的性能开销吗?还是您的数据有什么不同? (通过快速测试,same 列表的 100K 副本会极大地加快 pickle 的速度,但 json 根本不会;您正在处理的是那种事情吗?)
  • 无论如何,如果你想要一些只适用于受限类型白名单的东西,从 YAML 的安全子集开始并为你的白名单添加显式处理程序(而不是使用可扩展的 YAML 并尝试限制它)应该与您需要的一样安全。但它可能不会比您可能考虑的自定义扩展 JSON 更快;它甚至可能更慢。只是 YAML 被设计为以这种方式扩展,而 JSON 不是(这意味着,例如,通用可扩展 YAML 解析器将能够读取您的数据以进行调试),因此可能值得尝试。跨度>
  • 最后,你确定限制类型就足够了吗?很可能任何能够掌握管道的人都可以创建巨大的整数或其他东西来拒绝您的代码;如果字符串代表任何类型的外部资源,如 URL 或路径名,它们可以传递任何他们想要的字符串;等等这样可以吗?
  • 字典很好,但 numpy 数组和 pandas DataFrames(基于 numpy 数组)似乎不太好。对于 1000x100 的 DataFrame,转储到 JSON 需要 15 毫秒,加载需要 65 毫秒。通过 pickle 倾倒需要 250 微秒(不是毫秒),加载需要 150 微秒。
  • 谢谢。我会看看 YAML。 DoS 也是值得考虑的事情。

标签: python security sandbox pickle python-3.3


【解决方案1】:

听起来您对 JSON 的唯一真正问题是您编码 NumPy 数组(和 Pandas 表)的方式。 JSON 不适合您的用例 - 不是因为它处理 NumPy 数据的速度很慢,而是因为它是一种基于文本的格式,而且您有很多数据更容易以非基于文本的格式进行编码。

所以,我将在下面向您展示解决所有 JSON 问题的方法……但我建议使用不同的格式。

两种主要的“二进制 JSON”格式,BJSONBSON,旨在提供 JSON 的大部分优点(简单、安全、动态/无模式、可遍历等),同时还可以直接嵌入二进制数据。 (在这种情况下,它们也是二进制而不是文本格式对您来说并不重要。)我相信Smile 也是如此,但我从未使用过它。

这意味着,与 JSON 可以轻松挂钩任何可以简化为字符串、浮点数、列表和字典的方式一样,BJSON 和 BSON 可以轻松挂钩任何可以简化为字符串、浮点数、列表、字典、和字节字符串。因此,当我展示如何将 NumPy 编码/解码为字符串时,同样适用于字节字符串,但最后没有所有额外步骤。

BJSON 和 BSON 的缺点是它们不是人类可读的,并且没有那么广泛的支持。


我不知道您当前是如何对数组进行编码的,但从时间来看,我怀疑您正在使用 tolist 方法或类似方法。那肯定会很慢,而且很大。如果您在任何地方存储f8 值以外的任何值,它甚至会丢失信息(因为 JSON 理解的唯一数字是 IEEE 双精度数)。解决办法是编码成字符串。

NumPy 有一种文本格式,它会更快,而且不会有损,但仍可能比您想要的更慢和更大。

它还具有二进制格式,这很棒……但没有足够的信息来恢复您的原始数组。

所以,让我们看看 pickle 使用什么,你可以通过在任何对象上调用 __reduce__ 方法来查看:基本上,它是类型、形状、dtype、一些标志告诉 NumPy 如何解释原始数据,然后是二进制格式的原始数据。您实际上可以自己编码__reduce__ 数据——事实上,这样做可能是值得的。但是为了说明起见,让我们做一些更简单的事情,理解它只能在ndarray 上工作,并且不能在具有不同字节序的机器上工作(或者像符号大小整数或非 IEEE 浮点数这样的罕见情况)。

def numpy_default(obj):
    if isinstance(obj, np.ndarray):
        return {'_npdata': obj.tostring(), 
                '_npdtype': obj.dtype.name,
                '_npshape': obj.shape}
    else:
        return json.dumps(obj)

def dumps(obj):
    return json.dumps(obj, default=numpy_default)

def numpy_hook(obj):
    try:
        data = obj['_npdata']
    except AttributeError:
        return obj
    return np.fromstring(data, obj['_npdtype']).reshape(obj['_npshape'])

def loads(obj):
    return json.loads(obj, object_hook=numpy_hook)

唯一的问题是np.tostring 给你'bytes' 对象,Python 3 的json 不知道如何处理。

如果您使用的是 BJSON 或 BSON 之类的东西,您可以在此处停止。但是对于 JSON,您需要字符串。

您可以很容易地解决这个问题,如果不巧的话,通过使用映射每个单字节字符的任何编码(例如 Latin-1)“解码”字节:将 obj.tostring() 更改为 obj.tostring().decode('latin-1')data = obj['_npdata'] 更改为 data = obj['_npdata'].encode('latin-1') .通过 UTF-8 编码伪造的 Latin-1 字符串会浪费一点空间,但这还不错。

不幸的是,Python 将使用 Unicode 转义序列对每个非 ASCII 字符进行编码。您可以通过在转储上设置ensure_ascii=False 和在负载上设置strict=False 来关闭它,但它仍会将控制字符编码为6 字节序列。这会使随机数据的大小增加一倍,而且效果会更糟——例如,全零数组将大 6 倍!

曾经有一个技巧可以解决这个问题,但在 3.3 中,它不起作用。您可以做的最好的事情是对json 包进行分叉或猴子补丁,这样它就可以让您在给定ensure_ascii=False 时传递控制字符,您可以这样做:

json.encoder.ESCAPE = re.compile(r'"')

这很 hacky,但它确实有效。


无论如何,希望这足以让您入门。

【讨论】:

  • 非常感谢。我会再玩一些(在接受之前稍等片刻,看看其他人是否加入),但你帮了大忙。我进入了 Pandas 酸洗,它似乎基本上在其 numpy 数组上调用 reduce,您向我展示了如何使用 BSON 非常有效地序列化(我之前确实使用过 JSON 和列表)。 Pandas 中有一些内部结构可以将 DataFrame 分解为同质的 numpy 数组,所以我应该会顺利进行!
  • 类型越复杂,您可能越想依靠__reduce__ 而不是从头开始编写所有内容。例如,ndarray.__reduce__ 提供的其他值可让您区分小端和大端数据,或区分 ndarraymatrix 等。您只需编写手动代码将这些值映射到字符串然后返回到您想要处理的类型(希望是简短的静态白名单)的类型/构造函数,以确保没有人可以欺骗您反序列化您不想要的东西。
  • 再次感谢。如果我用旧数组的形状和 dtype 构造一个新的 numpy 数组,并尝试newarray.__setstate__(oldarray.__reduce__()),我会得到一个TypeError: must be sequence of length 4, not 3。我如何从减少到设置状态? (我也就此问过一个单独的 Pandas 特定问题。)
  • @user2461398: __setstate__ 使用来自__getstate__ 的值,而不是__reduce__。 pickle 文档解释了 __reduce__ 的元组形式的含义。基本上,你做obj = r[0](*r[1]); obj.__setstate__(r[2])。但是,如果您查看这些值是什么,您可以做一些更简单甚至更安全的事情。首先,obj = r[1][0](*r[1][1:]) 跳过隐藏的_reconstruct 调用。而且,您还可以查看r[2] 中的state info)——它是pickle 版本、形状、dtype、行顺序标志和原始数据。
猜你喜欢
  • 2013-01-10
  • 1970-01-01
  • 1970-01-01
  • 2010-11-08
  • 2011-06-20
  • 1970-01-01
  • 2020-10-23
  • 2014-06-20
  • 2012-09-03
相关资源
最近更新 更多