Title: http.client cannot send non-ASCII request lines
Type: behavior Stage: patch review
Components: Library (Lib) Versions: Python 3.8, Python 3.7
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: eric.snow, jaraco, mcepl, ned.deily, orsenthil, tburke, webknjaz, xtreak
Priority: normal Keywords: patch

Created on 2019-03-12 20:33 by tburke, last changed 2019-09-21 08:55 by jaraco.

Pull Requests
URL Status Linked Edit
PR 12314 closed tburke, 2019-03-13 23:59
PR 12315 closed tburke, 2019-03-13 23:59
PR 16321 open jaraco, 2019-09-21 08:55
Messages (12)
msg337802 - (view) Author: Tim Burke (tburke) * Date: 2019-03-12 20:33
While the RFCs are rather clear that non-ASCII data would be out of spec,

* that doesn't prevent a poorly-behaved client from sending non-ASCII bytes on the wire, which means
* as an application developer, it's useful to be able to mimic such a client to verify expected behavior while still using stdlib to handle things like header parsing, particularly since
* this worked perfectly well on Python 2.

The two most-obvious ways (to me, anyway) to try to send a request for /你好 (for example) are

    # Assume it will get UTF-8 encoded, as that's the default encoding
    # for urllib.parse.quote()
    conn.putrequest('GET', '/\u4f60\u597d')

    # Assume it will get Latin-1 encoded, as
    #   * that's the encoding used in http.client.parse_headers(),
    #   * that's the encoding used for PEP-3333, and
    #   * it has a one-to-one mapping with bytes
    conn.putrequest('GET', '/\xe4\xbd\xa0\xe5\xa5\xbd')

both fail with something like

    UnicodeEncodeError: 'ascii' codec can't encode characters in position ...

Trying to pre-encode like

    conn.putrequest('GET', b'/\xe4\xbd\xa0\xe5\xa5\xbd')

at least doesn't raise an error, but still does not do what was intended; rather than a request line like

    GET /你好 HTTP/1.1



depending on how you choose to interpret the bytes), the server gets

    GET b'/\xe4\xbd\xa0\xe5\xa5\xbd' HTTP/1.1

The trouble comes down to -- we don't actually have any control over what the caller passes as the url (so the assumption doesn't hold), nor do we know anything about the encoding that was *intended*.

One of three fixes seems warranted:

* Switch to using Latin-1 to encode instead of ASCII (again, leaning on the precedent set in parse_headers and PEP-3333). This may make it too easy to write an out-of-spec client, however.
* Continue to use ASCII to encode, but include errors='surrogateescape' to give callers an escape hatch. This seems like a reasonably high bar to ensure that the caller actually intends to send unquoted data.
* Accept raw bytes and actually use them (rather than their repr()), allowing the caller to decide upon an appropriate encoding.
msg351834 - (view) Author: Jason R. Coombs (jaraco) * (Python committer) Date: 2019-09-11 11:50
Thank you Tim for the reasoned issue and proposed solutions.

After reviewing these proposals with @eric.snow, we've decided that this approach is dangerous in that the proposed approaches has the potential to expose users unexpectedly to non-compliant behavior, where as currently they are assured compliance. Instead, we would like to see a more explicit opt-in, such as through a separate method or through a setting on the call and/or client object.

Consider instead a solution that implements both `.putrequest` and `.putrequest_raw` or `.putrequest_allow_invalid_bytes` that sends a clear signal to the user that they're bypassing the default protections.

Or consider another approach where HTTPConnection implements an `_encode_request()` method that a subclass with a specialized need could override.

Would either of those approaches suit your use-case?
msg352023 - (view) Author: Tim Burke (tburke) * Date: 2019-09-11 20:55
Fair enough. Seems kinda related to -- looks like it was a fun one ;-)

I think either approach would work for me; my existing work-around doesn't preclude either, particularly since I want it purely for testing purposes.

For a bit of context, I work on a large-ish project (few hundred kloc if you include tests) that recently finished porting from python 2.7 to 3.6 & 3.7. As part of that process I discovered and worked around it in That was a prerequisite for running tests under py2 against a server running py3, but this bug complicated us running the *tests* under py3 as well. I eventually landed on as a work-around.

I could probably take another stab at a fix if you like, though I'm not entirely sure when I'll get to it at the moment.
msg352340 - (view) Author: Jason R. Coombs (jaraco) * (Python committer) Date: 2019-09-13 14:26
I believe this issue is more recent and widespread than I originally thought. I realized today that CherryPy is affected by this issue and [disabled nightly tests]( as a result. It looks like the changed behavior was introduced in Python 3.7.4, so I expect stable releases to start failing also now. In that case, the web framework wishes to test that a null byte transmitted by the client is handled properly in the server, and without a patch, it's not possible for a project like cherrypy to transmit the invalid request to ensure a proper invalid response.

I see now that the previously proposed solution wouldn't even address this use-case, as the null byte would still have been excluded.
msg352342 - (view) Author: Jason R. Coombs (jaraco) * (Python committer) Date: 2019-09-13 14:56
I've started drafting a patch at
msg352452 - (view) Author: Jason R. Coombs (jaraco) * (Python committer) Date: 2019-09-14 20:43
As I considered a patch for this, I realized there are actually two issues, the one in the title "http.client cannot send non-ASCII request lines" but also "the protection for invalid requests prevents usage to generate invalid requests". The former issue was new with Python 3 while the latter was introduced with issue30458. I can't decide if we should handle these two issues separately, or address them together in this ticket.
msg352597 - (view) Author: Ned Deily (ned.deily) * (Python committer) Date: 2019-09-17 05:01
See msg352596 in Issue30458 for discussion of whether the regression should be considered a "release blocker" for the imminent 3.7.5 and 3.5.8 releases.
msg352711 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-09-18 08:42
Is there a reason why cherrypy doesn't URL encode the null byte in this test as per comment : ?

The original fix was adopted from golang ( and as per the cherrypy test the golang client also should be forbidding invalid control character :

Also as per the discussion one of our own stdlib tests were depending on this behavior in the http client and had to be changed :

The change made in Issue30458 also affects urllib3 test in 3.7.4 causing CI failure and the related discussion to encode URLs : . urllib3 issue : . 

IIRC urllib3 was also patched for this vulnerability in similar manner as part of larger refactor : . I didn't verify yet, it's unreleased and how urllib3 client behaves before and after patch for the CRLF characters and similar tests that use urllib3 as client.

Applying the URL encoding patch to cherrypy I can verify that the behavior is also tested

diff --git a/cherrypy/test/ b/cherrypy/test/
index cdd821ae..85a51cf3 100644
--- a/cherrypy/test/
+++ b/cherrypy/test/
@@ -398,7 +398,8 @@ class StaticTest(helper.CPWebCase):
         self.assertInBody("I couldn't find that thing")

     def test_null_bytes(self):
-        self.getPage('/static/\x00')
+        from urllib.parse import quote
+        self.getPage(quote('/static/\x00'))
         self.assertStatus('404 Not Found')


After patch the test passes and ValueError is also raised properly as the null byte is decoded in the server and using it in os.stat to resolve null byte path.

Breakages were expected in the discussion as adopting the fix from golang. Giving an option like a parameter to bypass the validation was also discussed in the linked but giving an option would also mean it could be abused or missed.
msg352725 - (view) Author: Sviatoslav Sydorenko (webknjaz) * Date: 2019-09-18 13:05
@xtreak the encoded null-byte test would be an extra test case to consider. It is reasonable to test as many known invalid sequences as possible. Changing that byte to encoded notation would just replace one test with another effectively changing the semantics of it.

To me, it's quite weird that it's considered a CVE at all: it's happening on the client side and it doesn't prevent the user from just feeding the proper bytes right into the socket so why overcomplicate things?
msg352739 - (view) Author: Jason R. Coombs (jaraco) * (Python committer) Date: 2019-09-18 16:15
I've created issue38216 to address the issue of sending invalid bytes in the request line, separate from the original intention and title issue about sending non-ASCII.
msg352743 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-09-18 16:40
> @xtreak the encoded null-byte test would be an extra test case to consider. It is reasonable to test as many known invalid sequences as possible. Changing that byte to encoded notation would just replace one test with another effectively changing the semantics of it.

Agreed that it's slightly different but I also admit I was also relying on the fact that encoded request will also be decoded by the server as done in most web frameworks to act upon the decoded payload. There could be systems where raw null byte might need to be transferred where the receiving system might not decode the payload but it seemed to be a workaround to ensure the required exception of serving null byte included static file was handled properly by CherryPy and also seemed to be adopted by others like urllib3.

> To me, it's quite weird that it's considered a CVE at all: it's happening on the client side and it doesn't prevent the user from just feeding the proper bytes right into the socket so why overcomplicate things?

The impacted function in http is used by functions like urllib.urlopen which is often feeded with input from the user that is not validated properly with some expectation that the function itself might handle the URL parsing and is safe enough. In this case it could lead to things like SSRF where malicious input could end up executing arbitrary code in some of the systems like Redis as demonstrated in the report. As for the CVE in question it was originally reported to golang and the same attack was also found out to be vulnerable. The fix adopted also seemed to have address few other security reports of similar nature.

Thanks Jason, as issue38216 is now a separate issue we can discuss around there. I guess this issue then is not a 3.7 only regression anymore since the original issue reported predates 3.7.
msg352750 - (view) Author: Jason R. Coombs (jaraco) * (Python committer) Date: 2019-09-18 18:43
That's right - no longer a 3.7 regression here.
Date User Action Args
2019-09-21 08:55:08jaracosetpull_requests: + pull_request15899
2019-09-18 18:43:20jaracosetkeywords: - 3.7regression

messages: + msg352750
2019-09-18 16:40:38xtreaksetmessages: + msg352743
2019-09-18 16:15:39jaracosetmessages: + msg352739
2019-09-18 13:05:58webknjazsetmessages: + msg352725
2019-09-18 08:42:16xtreaksetmessages: + msg352711
2019-09-17 05:01:27ned.deilysetkeywords: + 3.7regression
nosy: + ned.deily
messages: + msg352597

2019-09-15 10:53:15xtreaksetnosy: + xtreak
2019-09-14 21:23:32webknjazsetnosy: + webknjaz
2019-09-14 20:43:36jaracosetmessages: + msg352452
2019-09-13 21:37:27mceplsetnosy: + mcepl
2019-09-13 14:56:40jaracosetmessages: + msg352342
2019-09-13 14:26:06jaracosetmessages: + msg352340
2019-09-11 20:55:44tburkesetmessages: + msg352023
2019-09-11 11:50:34jaracosetnosy: + eric.snow, jaraco
messages: + msg351834
2019-03-13 23:59:52tburkesetpull_requests: + pull_request12289
2019-03-13 23:59:19tburkesetkeywords: + patch
stage: patch review
pull_requests: + pull_request12288
2019-03-13 10:18:31SilentGhostsetnosy: + orsenthil

versions: - Python 3.4, Python 3.5, Python 3.6
2019-03-12 20:33:23tburkecreate