有没有办法用“unicode”绕过 Django 的 XSS 转义?
目前尚不清楚幻灯片到底指的是什么。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 的出色回答列出了通过编码问题替代用户输入<
的一些缺点;<
django 默认会正确处理这些。
它是 100% 万无一失的吗?不,它们仍然为开发人员提供了足够的可配置性来执行不安全的操作,例如将用户输入插入 onclick 操作,绕过自动转义(通过mark_safe()
函数或{{ user_input|safe }}
在模板中),或允许用户输入到不安全的位置:例如,链接或在 eval'd javascript 中。当然,如果不对每个模板进行深入的编译/语义分析,几乎不可能做更多的事情。
对于感兴趣的人,转义代码在django/utils/html.py中非常易读。(我的链接指向当前的开发版本;但我的复制粘贴来自 django 1.2。开发版本和 1.2 版本之间的主要区别在于它们重命名force_unicode
为force_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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", '''))
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)
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