Skip to content

Commit 4e72ec4

Browse files
committed
Allow more characters in header values
The RFC says we should reject any header value that contains control characters. But apparently in the real world, you have to both accept and produce these sometimes (e.g. Google Analytics cookies use them). As a compromise, we now accept most control characters, but continue to disallow NUL (\x00) and all whitespace (\t\n\r\f\v and space), except that space and tab are allowed inside header values when surrounded by non-whitespace characters. Closes: gh-57, gh-58
1 parent da8a994 commit 4e72ec4

File tree

5 files changed

+50
-6
lines changed

5 files changed

+50
-6
lines changed

docs/source/changes.rst

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ Bug fixes:
1010

1111
* Always return headers as ``bytes`` objects (`#60
1212
<https://github.com/python-hyper/h11/issues/60>`__)
13+
* Allow a broader range of characters in header values. This violates
14+
the RFC, but is apparently required for compatibility with
15+
real-world code, like Google Analytics cookies (`#57
16+
<https://github.com/python-hyper/h11/issues/57>`__, `#58
17+
<https://github.com/python-hyper/h11/issues/58>`__)
1318

1419
Other changes:
1520

h11/_abnf.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,14 @@
4444
# See: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
4545
#
4646
# So our definition of field_content attempts to fix it up...
47-
vchar_or_obs_text = r"[\x21-\xff]"
47+
#
48+
# Also, we allow lots of control characters, because apparently people assume
49+
# that they're legal in practice (e.g., google analytics makes cookies with
50+
# \x01 in them!):
51+
# https://github.com/python-hyper/h11/issues/57
52+
# We still don't allow NUL or whitespace, because those are often treated as
53+
# meta-characters and letting them through can lead to nasty issues like SSRF.
54+
vchar_or_obs_text = r"[^\x00\s]"
4855
field_vchar = vchar_or_obs_text
4956
field_content = r"{field_vchar}+(?:[ \t]+{field_vchar}+)*".format(**globals())
5057

h11/tests/test_events.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,17 @@ def test_events():
8282
http_version="1.0")
8383

8484
# Header values are validated
85-
with pytest.raises(LocalProtocolError):
86-
req = Request(method="GET", target="/",
87-
headers=[("Host", "a"), ("Foo", " asd\x00")],
88-
http_version="1.0")
85+
for bad_char in "\x00\r\n\f\v":
86+
with pytest.raises(LocalProtocolError):
87+
req = Request(method="GET", target="/",
88+
headers=[("Host", "a"), ("Foo", "asd" + bad_char)],
89+
http_version="1.0")
90+
91+
# But for compatibility we allow non-whitespace control characters, even
92+
# though they're forbidden by the spec.
93+
Request(method="GET", target="/",
94+
headers=[("Host", "a"), ("Foo", "asd\x01\x02\x7f")],
95+
http_version="1.0")
8996

9097
ir = InformationalResponse(status_code=100, headers=[("Host", "a")])
9198
assert ir.status_code == 100

h11/tests/test_headers.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ def test_normalize_and_validate():
1818
assert "foo bar" in str(excinfo.value)
1919
with pytest.raises(LocalProtocolError):
2020
normalize_and_validate([(b"foo\x00bar", b"baz")])
21+
# Not even 8-bit characters:
22+
with pytest.raises(LocalProtocolError):
23+
normalize_and_validate([(b"foo\xffbar", b"baz")])
24+
# And not even the control characters we allow in values:
25+
with pytest.raises(LocalProtocolError):
26+
normalize_and_validate([(b"foo\x01bar", b"baz")])
2127

2228
# no return or NUL characters in values
2329
with pytest.raises(LocalProtocolError) as excinfo:
@@ -29,7 +35,13 @@ def test_normalize_and_validate():
2935
normalize_and_validate([("foo", "bar\x00baz")])
3036
# no leading/trailing whitespace
3137
with pytest.raises(LocalProtocolError):
32-
normalize_and_validate([("foo", " barbaz ")])
38+
normalize_and_validate([("foo", "barbaz ")])
39+
with pytest.raises(LocalProtocolError):
40+
normalize_and_validate([("foo", " barbaz")])
41+
with pytest.raises(LocalProtocolError):
42+
normalize_and_validate([("foo", "barbaz\t")])
43+
with pytest.raises(LocalProtocolError):
44+
normalize_and_validate([("foo", "\tbarbaz")])
3345

3446
# content-length
3547
assert (normalize_and_validate([("Content-Length", "1")])

h11/tests/test_io.py

+13
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,19 @@ def test_reject_garbage_in_header_line():
388388
b"Host: foo\x00bar\r\n\r\n",
389389
None)
390390

391+
# https://github.com/python-hyper/h11/issues/57
392+
def test_allow_some_garbage_in_cookies():
393+
tr(READERS[CLIENT, IDLE],
394+
b"HEAD /foo HTTP/1.1\r\n"
395+
b"Host: foo\r\n"
396+
b"Set-Cookie: ___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900\r\n"
397+
b"\r\n",
398+
Request(method="HEAD", target="/foo",
399+
headers=[
400+
("Host", "foo"),
401+
("Set-Cookie", "___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900"),
402+
]))
403+
391404
def test_host_comes_first():
392405
tw(write_headers,
393406
normalize_and_validate([("foo", "bar"), ("Host", "example.com")]),

0 commit comments

Comments
 (0)