0x00前言
最近,python官方修了一个漏洞,是关于参数分离不应该使用分号进行分离,容易造成XSS攻击的讨论,感兴趣的师傅可以看看当时的讨论https://bugs.python.org/issue42967
但是W3C是推荐使用分号进行分割的,原话是这样说的https://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2
We recommend that HTTP server implementors, and in particular, CGI implementors support the use of ";" in place of "&" to save authors the trouble of escaping "&" characters in this manner.
同时Django页进行了修复,但是修复的方式非常有意思,因此来谈谈这次修复的细节和对漏洞的感悟。
0x01 漏洞修复
使用分号;进行分割情况很早就有人提起过了,Django因为分号分离参数出现的问题也有很多人问过,stackoverflow上面就不止一次有人提起过。https://stackoverflow.com/questions/34408188/what-pattern-i-need-for-url-with-semicolon-in-url-in-django
这个问题在跟Django安全团队讨论缓存RCE的时候就注意到了,当时Google了一遍,发现是历史问题,也就没注意。。
本次漏洞出现的模块为urllib.parse
的parse_qsl方法,修复方法很简单,即使用split分割url参数时删除;
的字符即可。但是Django的修复仅仅只修改了单元测试的文件,具体如下
从源码来看,Django使用了urllib的parse_qsl方法来进行参数分割,但实际上,在Django 3.1.7之前(不包含3.1.7)使用的是独立的一个分割的代码。位于文件django/utils/http.py
文件中的limited_parse_qsl
函数
...
from django.utils.regex_helper import _lazy_re_compile
...
FIELDS_MATCH = _lazy_re_compile('[&;]')
def limited_parse_qsl(qs, keep_blank_values=False, encoding='utf-8',
errors='replace', fields_limit=None):
...
if fields_limit:
pairs = FIELDS_MATCH.split(qs, fields_limit)
if len(pairs) > fields_limit:
raise TooManyFieldsSent(
'The number of GET/POST parameters exceeded '
'settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.'
)
else:
pairs = FIELDS_MATCH.split(qs)
r = []
for name_value in pairs:
if not name_value:
continue
nv = name_value.split('=', 1)
if len(nv) != 2:
# Handle case of a control-name with no equal sign
if keep_blank_values:
nv.append('')
else:
continue
if nv[1] or keep_blank_values:
name = nv[0].replace('+', ' ')
name = unquote(name, encoding=encoding, errors=errors)
value = nv[1].replace('+', ' ')
value = unquote(value, encoding=encoding, errors=errors)
r.append((name, value))
return r
URL通过对FIELDS_MATCH的方法的split参数达到对地址符号和分号的分割,这也是3.1.7版本之前使用独立代码来分割参数的代码段。而在去年9月3号的时候,官方更新了一个细小的升级,标题是从python3.8版本之后使用urllib.parse.parse_qsl()来替代Django自己的limited_parse_qsl函数,这项提议发起与2019年11月,但是遭到一些成员的反对,首先目前的代码能够使用,其次使用新的补丁对核心代码进行更改会影响系统的稳定性,或许是当时情况下python的版本还没有普及到3.8,因此该项提议一直被搁置。一直到2020年1月份,Django官方准备在明年4月份推出3.2版本,并且将python3.8设置为最小支持语言的时候,该项PR被重新审查,最终在9月份完成merge,但是在此次改动上,由于还没有发布正式版,该版本为一个过度版本。具体的讨论,感兴趣的师傅可以看commithttps://github.com/django/django/pull/12017,部分代码如下
可以看到FIELDS_MATCH方法被删除,limited_parse_qsl函数更名为parse_qsl,其定义为首先引入urllib.parse.parse_qsl
方法,如果该包没有max_num_fields参数之后则引入django.utils.parse_qsl
作为替代,max_num_fields的作用是限制split参数分离的最大长度,并且加上了注释。此段代码将在Django的最小支持版本为3.8的版本之后删除。回看代码,可以看到该段代码依旧对分号进行了分割。
之后,在后两个改进的时候在http.py文件中删除了is_safe_url()以及urlquote系列的函数以更好支持urllib原生的参数分离,最终在2021年2月10日完成了对python3.8版本后的urllib的适配,删除了http.py文件下的parse_sql函数,具体commit在https://github.com/django/django/commit/ec0ff406311de88f4e2a135d784363424fe602aa#diff-a73a5062f47e9c4a504cccfe1674ec080ecc7f515d0d53885e6472704236efd2,同时也标志着CVE-2021-23336修复的结束。
当前的pip安装版本3.1.7虽然保留着FIELDS_MATCH参数,但是删除了;作为分离参数的字符
0x02浅谈漏洞
在早期的W3C标准上有着使用分号进行参数分离的标准,用分号分离url的问题也是早在2010年就有人在stackoverflow上面提问,并且有了36k的访问记录
查看sof上的回答和bug底下讨论,其大意为W3C的版本是1999年办法的,现已经过时,而在后期2014标准的叙述为
Let strings be the result of strictly splitting the string payload on U+0026 AMPERSAND characters (&).
不再提及使用;作为分离参数的字符。最终权衡安全性和开发者的便捷性之后,开发者Senthil Kumaran给出的答复为在python3.10以及之后的版本删除将;作为参数分离的字符
0xff Reference
- https://github.com/django/django/commit/0ad9fa02e07b853003b3c2244d1015620705f020
- https://docs.python.org/3/library/urllib.parse.html#urllib.parse.parse_qsl
- https://pypi.org/project/Django/3.1.6/
- https://github.com/django/django/commits/master/django/utils/http.py
- https://github.com/django/django/commit/0ad9fa02e07b853003b3c2244d1015620705f020
- https://snyk.io/blog/cache-poisoning-in-popular-open-source-packages/