Skip to content

Commit eec3423

Browse files
authored
Merge pull request #68 from njsmith/allow-more-chars-in-header-values
Allow more characters in header values
2 parents da8a994 + 4e72ec4 commit eec3423

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)