blob: 05e88a84fd8db78b53b6c7c818e99e1b4a24c0ab [file]
import socket
from threading import Thread
import pytest
from pyhttpd.conf import HttpdConf
from .env import TCPFaker
class _StatusLineBackend(TCPFaker):
"""Backend that sends various status line formats."""
def __init__(self, host, port, mode="final-status-sep-cr"):
super().__init__(host, port)
self._mode = mode
def _make_response(self, data):
if self._mode == "final-status-sep-cr":
return (
b"HTTP/1.1 200\rX-Foobar: abc\r\n"
b"Content-Length: 2\r\n"
b"Content-Type: text/plain\r\n"
b"\r\n"
b"OK"
)
elif self._mode == "interim-status-cr":
return (
b"HTTP/1.1 103 Early\rX-Foobar: abc\r\n"
b"X-Early: whatever\r\n"
b"\r\n"
b"HTTP/1.1 200 OK\r\n"
b"Content-Length: 2\r\n"
b"Content-Type: text/plain\r\n"
b"\r\n"
b"OK"
)
elif self._mode == "interim-102":
return (
b"HTTP/1.1 102 Processing\r\n"
b"\r\n"
b"HTTP/1.1 200 OK\r\n"
b"Content-Length: 2\r\n"
b"Content-Type: text/plain\r\n"
b"\r\n"
b"OK"
)
return super()._make_response(data)
def _recv_all(sock, timeout=5):
sock.settimeout(timeout)
data = b""
while True:
try:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
except socket.timeout:
break
return data
class TestStatusLineCR:
"""Verify that bare CR in backend status lines is rejected.
Two cases:
1. Final response with CR at the separator position (byte 3 of status_line)
2. Interim 1xx response with CR in the reason phrase
"""
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
conf = HttpdConf(env)
conf.start_vhost(domains=[f"test1.{env.http_tld}"], port=env.http_port,
doc_root="htdocs", with_ssl=False)
conf.add([
f"ProxyPass / http://127.0.0.1:{env.http_port2}/",
f"ProxyPassReverse / http://127.0.0.1:{env.http_port2}/",
])
conf.end_vhost()
conf.install()
assert env.apache_restart() == 0
yield
def test_proxy_06_001_final_status_sep_cr(self, env):
"""A final response with CR at status-code separator must not
forward attacker text as the reason phrase."""
faker = _StatusLineBackend("127.0.0.1", env.http_port2,
mode="final-status-sep-cr")
faker.start()
try:
with socket.create_connection(('localhost', int(env.http_port))) as sock:
req = (
f"GET / HTTP/1.0\r\n"
f"Host: test1.{env.http_tld}\r\n"
f"\r\n"
)
sock.sendall(req.encode())
sock.shutdown(socket.SHUT_WR)
raw = _recv_all(sock)
finally:
faker.stop()
status_line = raw.split(b"\r\n")[0]
assert b"\r" not in status_line[:-1] if status_line.endswith(b"\r") else b"\r" not in status_line, \
f"bare CR in status line: {status_line!r}"
assert b"X-Foobar" not in status_line, \
f"attacker text in status line: {status_line!r}"
# Prevent 'f-string expression part cannot include a backslash'
hide_backslash = b'\\r\\n\\r\\n'
assert b"X-Foobar" not in raw.split(b"\r\n\r\n")[0], \
f"injected header in response headers: {raw.split(hide_backslash)[0]!r}"
env.httpd_error_log.ignore_recent(
lognos=["AH00957", "AH01106", "AH01114"]
)
def test_proxy_06_002_interim_status_cr(self, env):
"""An interim 103 response with CR in the reason phrase must not
forward the bare CR to the client."""
faker = _StatusLineBackend("127.0.0.1", env.http_port2,
mode="interim-status-cr")
faker.start()
try:
with socket.create_connection(('localhost', int(env.http_port))) as sock:
req = (
f"GET / HTTP/1.1\r\n"
f"Host: test1.{env.http_tld}\r\n"
f"Connection: close\r\n"
f"\r\n"
)
sock.sendall(req.encode())
sock.shutdown(socket.SHUT_WR)
raw = _recv_all(sock)
finally:
faker.stop()
# Split into individual response blocks. The 103 interim response
# comes before the final 200. Check every status line for bare CR.
lines = raw.split(b"\r\n")
for line in lines:
if line.startswith(b"HTTP/"):
assert b"\r" not in line, \
f"bare CR in status line: {line!r}"
assert b"X-Foobar" not in line, \
f"attacker text in status line: {line!r}"
headers_section = raw.split(b"\r\n\r\n")[0]
assert b"X-Foobar" not in headers_section, \
f"injected header in response: {headers_section!r}"
env.httpd_error_log.ignore_recent(
lognos=["AH01106"]
)
def test_proxy_06_003_interim_102_ok(self, env):
"""A well-formed 102 Processing interim response is forwarded
correctly, followed by the final 200."""
faker = _StatusLineBackend("127.0.0.1", env.http_port2,
mode="interim-102")
faker.start()
try:
with socket.create_connection(('localhost', int(env.http_port))) as sock:
req = (
f"GET / HTTP/1.1\r\n"
f"Host: test1.{env.http_tld}\r\n"
f"Connection: close\r\n"
f"\r\n"
)
sock.sendall(req.encode())
sock.shutdown(socket.SHUT_WR)
raw = _recv_all(sock)
finally:
faker.stop()
status_lines = [l for l in raw.split(b"\r\n")
if l.startswith(b"HTTP/")]
assert len(status_lines) == 2, \
f"expected 2 status lines (102 + 200), got {len(status_lines)}: {status_lines!r}"
assert b"102" in status_lines[0], \
f"first status line should be 102: {status_lines[0]!r}"
assert b"200" in status_lines[1], \
f"second status line should be 200: {status_lines[1]!r}"