Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,23 @@ def parse_request(self):
)
return False

# RFC 7230 section 3.3.3 rule 3: this handler does not implement
# a chunked decoder, so any Transfer-Encoding is rejected.
if self.headers.get('Transfer-Encoding'):
self.send_error(
HTTPStatus.BAD_REQUEST,
"Transfer-Encoding not supported")
return False

# RFC 7230 section 3.3.3 rule 4: reject duplicate Content-Length
# values that disagree.
cl_values = self.headers.get_all('Content-Length') or []
if len({v.strip() for v in cl_values}) > 1:
self.send_error(
HTTPStatus.BAD_REQUEST,
"Conflicting Content-Length values")
return False

conntype = self.headers.get('Connection', "")
if conntype.lower() == 'close':
self.close_connection = True
Expand Down
104 changes: 104 additions & 0 deletions Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,110 @@ def test_head_via_send_error(self):
self.assertEqual(b'', data)


class RFC7230FramingTestCase(BaseTestCase):
"""Exercise the framing checks added for RFC 7230 section 3.3.3."""

class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
default_request_version = 'HTTP/1.1'

def do_POST(self):
cl = self.headers.get('Content-Length')
n = int(cl) if cl and cl.isdigit() else 0
body = self.rfile.read(n) if n else b''
out = b'POST body=' + body + b'\n'
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'text/plain')
self.send_header('Content-Length', str(len(out)))
self.end_headers()
self.wfile.write(out)

def _send_raw(self, payload, timeout=2):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((self.HOST, self.PORT))
try:
sock.sendall(payload)
data = b''
try:
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
except TimeoutError:
pass
finally:
sock.close()
return data

def test_transfer_encoding_rejected(self):
# RFC 7230 section 3.3.3 rule 3 plus no chunked decoder.
data = self._send_raw(
b'POST / HTTP/1.1\r\n'
b'Host: 127.0.0.1\r\n'
b'Transfer-Encoding: chunked\r\n'
b'Content-Length: 5\r\n'
b'\r\n'
b'0\r\n\r\nGET /pwn HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n'
)
self.assertTrue(
data.startswith(b'HTTP/1.0 400') or data.startswith(b'HTTP/1.1 400'),
data[:80])
self.assertIn(b'Connection: close', data)
self.assertNotIn(b'/pwn', data)

def test_duplicate_content_length_rejected(self):
# RFC 7230 section 3.3.3 rule 4.
data = self._send_raw(
b'POST / HTTP/1.1\r\n'
b'Host: 127.0.0.1\r\n'
b'Content-Length: 4\r\n'
b'Content-Length: 0\r\n'
b'\r\n'
b'ABCD'
)
self.assertTrue(
data.startswith(b'HTTP/1.0 400') or data.startswith(b'HTTP/1.1 400'),
data[:80])
self.assertIn(b'Connection: close', data)

def test_duplicate_content_length_same_value_accepted(self):
# Two Content-Length headers with the same value are not a conflict
# per RFC 7230 section 3.3.3 rule 4.
data = self._send_raw(
b'POST / HTTP/1.1\r\n'
b'Host: 127.0.0.1\r\n'
b'Content-Length: 4\r\n'
b'Content-Length: 4\r\n'
b'\r\n'
b'ABCD'
)
self.assertTrue(
data.startswith(b'HTTP/1.0 200') or data.startswith(b'HTTP/1.1 200'),
data[:80])
self.assertIn(b"POST body=ABCD", data)

def test_keep_alive_post_pipeline(self):
# Regression: two pipelined POSTs with correct Content-Length
# both succeed on a single keep-alive connection.
data = self._send_raw(
b'POST / HTTP/1.1\r\n'
b'Host: 127.0.0.1\r\n'
b'Content-Length: 4\r\n'
b'\r\n'
b'ABCD'
b'POST / HTTP/1.1\r\n'
b'Host: 127.0.0.1\r\n'
b'Content-Length: 3\r\n'
b'\r\n'
b'XYZ'
)
self.assertEqual(data.count(b'HTTP/1.1 200'), 2)
self.assertIn(b'POST body=ABCD', data)
self.assertIn(b'POST body=XYZ', data)


class HTTP09ServerTestCase(BaseTestCase):

class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`http.server` now rejects requests that send any ``Transfer-Encoding``
header or that pair conflicting ``Content-Length`` values, since the module
does not implement a chunked decoder. Both rejections return
``400 Bad Request`` with ``Connection: close``, per :rfc:`7230#section-3.3.3`.
Loading