Two REDoS vulns in cpython
I ran my top-secret REDoS-finding engine over the python code in cpython and found two remotely-exploitable vulnerabilities. Making a request to a malicious web server leads to denial of service (approximately infinite CPU time).
http.cookiejar #
This issue (bpo38804) was serious because the vulnerable code could be as simple as:
import requests
requests.get("http://malicious/")
To exploit this, simply cause your target to hit a malicious server running the proof-of-concept code in my fix PR which sends massive Set-Cookie
response headers.
The underlying issue was the regular expression http.cookiejar.LOOSE_HTTP_DATE_RE
used to parse the Expires
field from Set-Cookie
response headers.
Ignoring the ?-optional capture groups, the original regex can be simplified to
\d+-\w+-\d+(\s*\s*\s*)$
Therefore, a long sequence of spaces can trigger bad performance.
LOOSE_HTTP_DATE_RE
backtracked if last character didn't match \s
or (?![APap][Mm]\b)[A-Za-z]+
.
Matching a malicious string such as
LOOSE_HTTP_DATE_RE.match("1-1-1" + (" " * 2000) + "!")
caused catastrophic backtracking. Timing on my computer when doubling the number of spaces:
n_spaces | seconds
512 .383
1024 3.02
2048 23.4
4096 184
8192 1700
As expected, it's approx O(n3). The maximum n_spaces
to fit in a Set-Cookie
header is 65506 which will take days.
I fixed this bug with my first contribution to python/cpython.
urllib.request #
Less code is vulnerable to this bug (bpo38826/bpo39503) as it requires that you are using an auth handler. Vulnerable client:
import urllib.request
opener = urllib.request.build_opener(
urllib.request.HTTPBasicAuthHandler()
)
opener.open("http://malicious/")
A malicious server just has to send back a 401 with a crafted WWW-Authenticate
header such as my proof-of-concept code.
The vulnerable regular expression is urllib.request.AbstractBasicAuthHandler.rx
:
rx = re.compile('(?:.*,)*[ \t]*([^ \t]+)[ \t]+'
'realm=(["\']?)([^"\']*)\\2', re.I)
The first line can act like:
(,*,)*(,+)[ \t]
showing that there are many different ways to match a long sequence of commas.
Input from the WWW-Authenticate
or Proxy-Authenticate
headers of HTTP responses will reach the regex via the http_error_auth_reqed
method as long as the header value starts with "basic "
.
We can craft a malicious input:
urllib.request.AbstractBasicAuthHandler.rx.search(
"basic " + ("," * 100) + "A"
)
Which causes catastrophic backtracking and takes a large amount of CPU time to process. I tested the length of time (seconds) to complete for different numbers of commas in the string:
n_commas | seconds
18 0.289
19 0.57
20 1.14
21 2.29
22 4.55
23 9.17
24 18.3
25 36.5
26 75.1
27 167
Showing an exponential relationship O(2x) !
The maximum length of comma string that can fit in a response header is 65509, which would take my computer just 6×1019706 years to complete. Compare this to the worst case of the cubic http.cookiejar
vulnerability above being measured in days.
Another researcher later requested CVE-2020-8492 for this vulnerability. It is in the process of being fixed.
- Previous: Big Data Lake, Big Data Leak
- Next: Exploit Grafana (CVE‑2019‑15043)