CVE-2021-23336 分号分割url问题的闲聊

  python

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