fix(proxy): recompute Content-Length when replaying a modified body (#816)

build_raw_request kept the Content-Length inherited from the captured
request, so replaying a modified body (repeat_request) emitted a request
whose declared length did not match the body — truncating the payload or
stalling the target. Drop any inherited Content-Length (case-insensitively)
and recompute it from the body actually being sent.

Adds tests covering a lengthened body, an emptied body, and the
no-inherited-header path.

Fixes #814

Co-authored-by: thejesh23 <thejesh23@users.noreply.github.com>
This commit is contained in:
Thejesh Reddy
2026-07-20 21:43:49 -04:00
committed by GitHub
co-authored by thejesh23
parent 8cd9abba21
commit 599f7c7526
2 changed files with 43 additions and 1 deletions
+6 -1
View File
@@ -187,7 +187,12 @@ def build_raw_request(
final_headers = {**headers}
final_headers.setdefault("Host", parsed.netloc)
final_headers.setdefault("User-Agent", "strix")
if body and "Content-Length" not in {k.title() for k in final_headers}:
# A Content-Length inherited from the captured request describes the ORIGINAL
# body; once the body is modified for replay it is stale. Drop any inherited
# value (case-insensitively) and recompute it from the body actually being
# sent, so the replayed request is never desynced (truncated / smuggled).
final_headers = {k: v for k, v in final_headers.items() if k.title() != "Content-Length"}
if body:
final_headers["Content-Length"] = str(len(body.encode("utf-8")))
lines = [f"{method.upper()} {path} HTTP/1.1"]
+37
View File
@@ -140,6 +140,43 @@ async def test_host_call_serializes_concurrent_calls() -> None:
assert state["max"] == 1
def _headers_named(raw: bytes, name: str) -> list[str]:
head = raw.decode("utf-8").split("\r\n\r\n", 1)[0]
return [
line.split(":", 1)[1].strip()
for line in head.split("\r\n")[1:]
if line.split(":", 1)[0].strip().lower() == name.lower()
]
def test_build_raw_request_recomputes_content_length_for_modified_body() -> None:
# The captured request declared Content-Length: 12 (original body); the
# replayed body is longer. The emitted request must carry exactly one
# Content-Length equal to the ACTUAL body length, or the target truncates
# the modified payload (or the connection desyncs).
body = '{"user":"a\' OR 1=1 -- injected long payload"}'
_conn, raw = caido_api.build_raw_request(
method="POST",
url="https://example.com/login",
headers={"content-length": "12", "Content-Type": "application/json"},
body=body,
)
sent_body = raw.decode("utf-8").split("\r\n\r\n", 1)[1]
assert sent_body == body
assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))]
def test_build_raw_request_drops_stale_content_length_for_empty_body() -> None:
# A body cleared to empty must not keep the inherited (non-zero) length.
_conn, raw = caido_api.build_raw_request(
method="POST",
url="https://example.com/x",
headers={"Content-Length": "12"},
body="",
)
assert _headers_named(raw, "Content-Length") == []
class _Ctx:
def __init__(self, context: Any) -> None:
self.context = context