文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

我来说在很早之前就应该发现这个明显的问题,但是直到最近,我才意识到 str.format 因为不可信的用户输入而产生的问题的严重性。这为绕过Jinja2沙盒并越权获取信息提供了一个方法,这也是我近期推送了一个安全更新的原因。

然而我认为跳出这个问题来讨论形势仍然是十分严峻的,需要指出的是,许多人并不关心数据被滥用有多简单。

核心问题

受到.Net的启发,Python自2.6版起支持了一种新的格式化字符串语法,这种格式化字符串的语法同时也被Rust以及其他编程语言所支持。.format()为这个语法提供了对字节字符串和Unicode字符串的支持(Python 3只支持Unicode字符串),同时具有更多可自定义功能的string.Formatter API也使用了部分代码。

第 1 段(可获 2.06 积分)

其中一项功能允许用户任意定位Positional参数和带关键字的参数。 然而,更有用的功能是你可以访问属性以及对象的Items。也就是后者,造成了本文所描述的问题。

比如你可以这么做:

>>> 'class of {0} is {0.__class__}'.format(42)
"class of 42 is <class 'int'>"

也就是说:只要能控制格式化字符串,就有能访问对象内部属性的潜在可能性。

第 2 段(可获 0.93 积分)

它在哪里发生?

首先我们需要指出为什么用户会有机会控制格式化字符串?下面列举了几个可能的情形:

  • 使用未验证过的翻译文件。很多支持多国语言的应用会使用字符串文件保存翻译,将格式化字符串存入翻译文件并使用新式的Python字符串格式化函数,这也是最有可能发生的,然而并不是所有开发者都会验证翻译文件。
  • 暴露给用户的配置文件。一些应用可能允许用户通过修改格式化字符串来控制输出格式。实际上,笔者曾见过一些如用户可以配置邮件通知内容、记录信息的格式或者其他网页应用的模板的情形。

危险性评级

如果仅有C解释器的对象传递到格式化字符串之中,那么相对来说还是安全的,毕竟最差的情形也就是可能被用户发现某个变量实际上是个整数。

然而一旦Python对象被传递到格式化字符串之中,事情就变得不那么有趣了。其原因就是Python函数暴露的东西不只是一点两点。下面这个假设的例子展示了一个网页应用的安装脚本可能泄露其安全密钥。

 

第 3 段(可获 1.8 积分)

它在哪里发生?

首先我们需要指出为什么用户会有机会控制格式化字符串?下面列举了几个可能的情形:

  • 使用未验证过的翻译文件。很多支持多国语言的应用会使用字符串文件保存翻译,将格式化字符串存入翻译文件并使用新式的Python字符串格式化函数,这也是最有可能发生的,然而并不是所有开发者都会验证翻译文件。
  • 暴露给用户的配置文件。一些应用可能允许用户通过修改格式化字符串来控制输出格式。实际上,笔者曾见过一些如用户可以配置邮件通知内容、记录信息的格式或者其他网页应用的模板的情形。

危险性评级

如果仅有C解释器的对象传递到格式化字符串之中,那么相对来说还是安全的,毕竟最差的情形也就是可能被用户发现某个变量实际上是个整数。

然而一旦Python对象被传递到格式化字符串之中,事情就变得不那么有趣了。其原因就是Python函数暴露的东西不只是一点两点。下面这个假设的例子展示了一个网页应用的安装脚本可能泄露其安全密钥。

CONFIG = {
    'SECRET_KEY': 'super secret key'
}

class Event(object):
    def __init__(self, id, level, message):
        self.id = id
        self.level = level
        self.message = message

def format_event(format_string, event):
    return format_string.format(event=event)

如果用户可以通过某种方式控制上述代码里的format_string变量,那么他有可能输入通过:

{event.__init__.__globals__[CONFIG][SECRET_KEY]} 

最终获得安全密钥。

第 4 段(可获 0.78 积分)

将格式化过程控制在沙盒之中

那么,如果你真的的确需要允许用户控制格式化字符串,如何才能够安全的使用它们呢?通过控制一些没有被官方文档详细记录的内部状态变量,来改变格式化字符串函数的行为。

from string import Formatter
from collections import Mapping

class MagicFormatMapping(Mapping):
    """
    这个类实现了一个Dummy Wrapper用以修补一个来自Python标准库提供格式化字符串功能的的漏洞

    参阅 http://bugs.python.org/issue13598 获知关于这么做的必要性。
    """

    def __init__(self, args, kwargs):
        self._args = args
        self._kwargs = kwargs
        self._last_index = 0

    def __getitem__(self, key):
        if key == '':
            idx = self._last_index
            self._last_index += 1
            try:
                return self._args[idx]
            except LookupError:
                pass
            key = str(idx)
        return self._kwargs[key]

    def __iter__(self):
        return iter(self._kwargs)

    def __len__(self):
        return len(self._kwargs)

# 这是一个必须的API,然而它并没有被文档记载,并且在不同版本的Python之间来回移动
try:
    from _string import formatter_field_name_split
except ImportError:
    formatter_field_name_split = lambda \
        x: x._formatter_field_name_split()

class SafeFormatter(Formatter):

    def get_field(self, field_name, args, kwargs):
        first, rest = formatter_field_name_split(field_name)
        obj = self.get_value(first, args, kwargs)
        for is_attr, i in rest:
            if is_attr:
                obj = safe_getattr(obj, i)
            else:
                obj = obj[i]
        return obj, first

def safe_getattr(obj, attr):
    # 详细描述:在2.x版本中同时需要禁用 func_globals,
    # 3.x版本中需要隐藏类似cr_frame的东西,
    # 所以,创建一个记录需要保密对象的列表是个好主意。
    # 下面仅仅是一个例子,禁用任何以下划线开头的属性。
    if attr[:1] == '_':
        raise AttributeError(attr)
    return getattr(obj, attr)

def safe_format(_string, *args, **kwargs):
    formatter = SafeFormatter()
    kwargs = MagicFormatMapping(args, kwargs)
    return formatter.vformat(_string, args, kwargs)

现在,你可以使用 safe_format() 方法来替代 str.format() 以安全地完成所需要实现的功能:

>>> '{0.__class__}'.format(42)
"<type 'int'>"
>>> safe_format('{0.__class__}', 42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __class__

 

第 5 段(可获 0.55 积分)

文章评论