有没有办法用“unicode”绕过 Django 的 XSS 转义?

信息安全 Web应用程序 javascript xss html 统一码
2021-08-12 11:12:44

Django(Python Web 框架)转义输出以防止 XSS(跨站点脚本)攻击。它将', ", <, >,替换&为其 HTML 安全版本。

然而,关于幻灯片共享的演示文稿(特别是幻灯片 № 13)说:

问题

  1. 任何其他 Unicode 将绕过此检查

我无法理解这种抱怨。是否有一些 unicode 字符不会被escape允许 XSS 的 Django 函数替换?我对unicode有点了解,但我想不出怎么做。

3个回答

目前尚不清楚幻灯片到底指的是什么。Django 的自动转义应该​​可以很好地防止文本内容中的 HTML 注入和正确引用的属性值。

没有其他 Unicode字符可以逃避 HTML 转义,但原则上存在可能被误解为错误 Unicode 编码的字节序列:

  • 如果浏览器决定将文档解释为 UTF-7,+ADw-则成为<(以及类似序列&"'>)的同义词,从而允许 HTML 元字符避免被转义。

  • 一些东亚多字节编码允许多字节序列中的尾随字节位于 0x00-0x7F 范围内,在该范围内它们可以被解释为 ASCII 字符,并且如果以这种方式处理则会错误转义。不过,通常这只会导致文本损坏而不是安全问题。

  • 无效的“超长”UTF-8 字节序列可能会被一些非常旧的浏览器解释为 ASCII(最初的 IE6 pre-SP1 和大约同时的 Opera)。这可以允许 HTML 元字符避免被转义,例如字节序列 0xC0 0xBC 表示<.

为了避免这些问题,您将 (a) 确保使用 UTF-8 提供您的文档Content-Type charset,并且 (b) 在内部将所有文本字符串保留为本机 Unicode 字符串,以便它们永远不会编码为无效的 UTF-8 序列。

由于 Django 应用程序在默认情况下倾向于这样做,因此 Django 模板的自动转义不太可能被 Unicode 问题打败。

当然,这并不是说 XSS 得到了普遍解决——你仍然必须避免滥用|safe、不带引号的属性、非 HTML 注入问题(如 JavaScript 字符串、CSS 属性、URL 参数)、HTML 内容嗅探、危险的 URL 方案(javascript:等al) 等等。但作为对模板中 HTML 注入的防御,它应该是合理的。

是的,至少存在三个此 XSS 过滤器失败的实例。XSS 很复杂,盲目的替换字符并不能解决这个问题。最明显的是,如果您在脚本标签中编写:

<script>
var x = alert(1);
</script>

如果您正在编写 href 或 iframe src,则可以使用javascript:URI:

<a href=javascript:alert(1)>alert</a>

如果您在 DOM 事件中编写,它也容易受到 xss 的攻击

<a href="doSomethingCool('userInput%27);sendHaxor(document.cookie);//');">Cool Link</a>

浏览器将在执行 JavaScript 事件之前自动解码%27(以及其他编码方法)。

Django 做了一些明智的事情来减少 XSS 的暴露。

默认情况下,Django 在任何地方都使用 unicode 和 UTF-8 编码,并且在对所有模板变量(默认完成)进行替换之前明智地强制使用 unicode 编码,以防止用户插入任意 HTML 元素。Django 允许开发人员使用设置更改编码DEFAULT_CHARSET,但会在整个应用程序中强制使用该编码,并且Content-Type: text/html; charset=utf-8默认情况下会插入 HTTP 响应标头(如果您返回不同的 content_type 或更改,则 'text/html' 和 'utf-8' 会更改)字符集)。此外,django 页面还将<meta http-equiv="content-type" content="text/html; charset=utf-8">在其基本模板和管理页面中进行设置,但再次为开发人员提供了不使用其基本模板的选项(并且开发人员自定义编写的模板可能未在元标记中定义字符集,或者更糟的是可能使用错误字符集)。所以虽然bobince 的出色回答列出了通过编码问题替代用户输入<的一些缺点;&lt;django 默认会正确处理这些。

它是 100% 万无一失的吗?不,它们仍然为开发人员提供了足够的可配置性来执行不安全的操作,例如将用户输入插入 onclick 操作,绕过自动转义(通过mark_safe()函数或{{ user_input|safe }}在模板中),或允许用户输入到不安全的位置:例如,链接或在 eval'd javascript 中。当然,如果不对每个模板进行深入的编译/语义分析,几乎不可能做更多的事情。

对于感兴趣的人,转义代码在django/utils/html.py中非常易读。(我的链接指向当前的开发版本;但我的复制粘贴来自 django 1.2。开发版本和 1.2 版本之间的主要区别在于它们重命名force_unicodeforce_text(在 py3 中,所有文本都是 unicode)并使其与 python 3 兼容(所有参考六)。)

基本上,转义函数在要在​​模板中呈现的每个变量上运行,并首先检查它是否可以正确编码,然后替换字符:&<>'"用它们的 HTML 转义等效项。还有转义 JS 的功能,尽管我认为必须在模板中手动调用{{ variable|escapejs }}.

def escape(html):
    """
    Returns the given HTML with ampersands, quotes and angle brackets encoded.
    """
    return mark_safe(force_unicode(html).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))
escape = allow_lazy(escape, unicode)

_base_js_escapes = (
    ('\\', r'\u005C'),
    ('\'', r'\u0027'),
    ('"', r'\u0022'),
    ('>', r'\u003E'),
    ('<', r'\u003C'),
    ('&', r'\u0026'),
    ('=', r'\u003D'),
    ('-', r'\u002D'),
    (';', r'\u003B'),
    (u'\u2028', r'\u2028'),
    (u'\u2029', r'\u2029')
)

# Escape every ASCII character with a value less than 32.
_js_escapes = (_base_js_escapes +
               tuple([('%c' % z, '\\u%04X' % z) for z in range(32)]))

def escapejs(value):
    """Hex encodes characters for use in JavaScript strings."""
    for bad, good in _js_escapes:
        value = mark_safe(force_unicode(value).replace(bad, good))
    return value
escapejs = allow_lazy(escapejs, unicode)

def conditional_escape(html):
    """
    Similar to escape(), except that it doesn't operate on pre-escaped strings.
    """
    if isinstance(html, SafeData):
        return html
    else:
        return escape(html)

并来自django/utils/encoding.py

def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'):
    """
    Similar to smart_unicode, except that lazy instances are resolved to
    strings, rather than kept as lazy objects.

    If strings_only is True, don't convert (some) non-string-like objects.
    """
    if strings_only and is_protected_type(s):
        return s
    try:
        if not isinstance(s, basestring,):
            if hasattr(s, '__unicode__'):
                s = unicode(s)
            else:
                try:
                    s = unicode(str(s), encoding, errors)
                except UnicodeEncodeError:
                    if not isinstance(s, Exception):
                        raise
                    # If we get to here, the caller has passed in an Exception
                    # subclass populated with non-ASCII data without special
                    # handling to display as a string. We need to handle this
                    # without raising a further exception. We do an
                    # approximation to what the Exception's standard str()
                    # output should be.
                    s = ' '.join([force_unicode(arg, encoding, strings_only,
                            errors) for arg in s])
        elif not isinstance(s, unicode):
            # Note: We use .decode() here, instead of unicode(s, encoding,
            # errors), so that if s is a SafeString, it ends up being a
            # SafeUnicode at the end.
            s = s.decode(encoding, errors)
    except UnicodeDecodeError, e:
        if not isinstance(s, Exception):
            raise DjangoUnicodeDecodeError(s, *e.args)
        else:
            # If we get to here, the caller has passed in an Exception
            # subclass populated with non-ASCII bytestring data without a
            # working unicode method. Try to handle this without raising a
            # further exception by individually forcing the exception args
            # to unicode.
            s = ' '.join([force_unicode(arg, encoding, strings_only,
                    errors) for arg in s])
    return s