Skip to content

Commit 8618d21

Browse files
authored
Merge pull request #484 from thearun85/fix/reject-duplicate-host-header
Reject duplicate Host headers per RFC 9112
2 parents dd9f1a7 + 5e12784 commit 8618d21

File tree

3 files changed

+29
-1
lines changed

3 files changed

+29
-1
lines changed

CONTRIBUTORS.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,5 @@ Contributors
150150
- Jonathan Vanasco, 2022-11-15
151151

152152
- Simon King, 2024-11-12
153+
154+
- Arun Raghunath, 2026-03-13

src/waitress/parser.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
find_double_newline,
3434
)
3535

36+
# A list of HEADERS that must not be duplicated per RFC 9112
37+
HEADERS_NO_DUPLICATES = frozenset({"HOST", "CONTENT_LENGTH"})
38+
3639

3740
def unquote_bytes_to_wsgi(bytestring):
3841
return unquote_to_bytes(bytestring).decode("latin-1")
@@ -237,6 +240,11 @@ def parse_header(self, header_plus):
237240
# RFC7230, don't strip the rest
238241
value = value.strip(b" \t")
239242
key1 = key.upper().replace(b"-", b"_").decode("latin-1")
243+
244+
# Reject duplicate 'Host' headers as per RFC 9112 section 3.2
245+
if key1 in HEADERS_NO_DUPLICATES and key1 in headers:
246+
raise ParsingError(f"Duplicate header: {key.decode('latin-1')}")
247+
240248
# If a header already exists, we append subsequent values
241249
# separated by a comma. Applications already need to handle
242250
# the comma separated values, as HTTP front ends might do

tests/test_parser.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@ def test_received_bad_host_header(self):
6363
self.assertTrue(self.parser.completed)
6464
self.assertIsInstance(self.parser.error, BadRequest)
6565

66+
def test_received_duplicate_host_header(self):
67+
# RFC 9112: MUST reject HTTP/1.1 requests with more than one Host header
68+
data = b"GET / HTTP/1.1\r\nHOST: test1.com\r\nHost: test2.com\r\n\r\n"
69+
result = self.parser.received(data)
70+
self.assertEqual(result, len(data))
71+
self.assertTrue(self.parser.completed)
72+
self.assertIsInstance(self.parser.error, BadRequest)
73+
self.assertTrue(self.parser.error.body.startswith("Duplicate header:"))
74+
75+
def test_received_duplicate_content_length_header(self):
76+
# RFC 7230: MUST reject requests with duplicate Content-Length headers
77+
data = b"GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 10\r\nContent-Length: 20\r\n\r\n"
78+
result = self.parser.received(data)
79+
self.assertEqual(result, len(data))
80+
self.assertTrue(self.parser.completed)
81+
self.assertIsInstance(self.parser.error, BadRequest)
82+
self.assertTrue(self.parser.error.body.startswith("Duplicate header:"))
83+
6684
def test_received_bad_transfer_encoding(self):
6785
data = (
6886
b"GET /foobar HTTP/1.1\r\n"
@@ -227,7 +245,7 @@ def test_parse_header_multiple_content_length(self):
227245
try:
228246
self.parser.parse_header(data)
229247
except ParsingError as e:
230-
self.assertIn("Content-Length is invalid", e.args[0])
248+
self.assertTrue(e.args[0].startswith("Duplicate header:"))
231249
else: # pragma: nocover
232250
self.assertTrue(False)
233251

0 commit comments

Comments
 (0)