resolve(proxy): recompute Content-Length when repeat_request replaces body

Problem:
apply_modifications() let modifications={"body": ...} replace the request
body but left the original Content-Length header untouched.
build_raw_request() only fills in Content-Length when the header is
absent, so the stale, smaller value from the original request was sent
on the wire alongside the new, larger body.

Impact:
A server reading exactly Content-Length bytes truncates the replayed
body (or stalls waiting for more), silently discarding the
modified/injected payload. This defeats parameter-tampering and
injection replays, which is the core use case of repeat_request, and
produces misleading negative results during testing.

Resolution:
When modifications["body"] is present, drop any existing Content-Length
header from the carried-over headers (matched case-insensitively),
unless the caller explicitly supplies Content-Length in this same call's
modifications["headers"]. This lets build_raw_request's existing
"fill in if absent" logic recompute the correct length from the new
body.

Testing:
Added tests/test_caido_api.py covering: stale header dropped on body
replacement, explicit caller-supplied Content-Length respected, headers
left untouched when body isn't modified, and an end-to-end check that
build_raw_request emits the recomputed length in the raw HTTP request.
Verified RED (failing on pre-fix code, reproducing the exact stale
Content-Length: 3 scenario from the bug report) before GREEN.

Resolves #603
This commit is contained in:
hardik-devops 2026-07-01 19:39:27 +05:30
parent f342808d2b
commit f4d2aea432
2 changed files with 68 additions and 0 deletions

View file

@ -261,6 +261,12 @@ def apply_modifications(
headers.update(modifications["headers"])
if "body" in modifications:
body = modifications["body"]
explicit_content_length = "Content-Length" in {
k.title() for k in modifications.get("headers", {})
}
if not explicit_content_length:
for key in [k for k in headers if k.title() == "Content-Length"]:
del headers[key]
if "cookies" in modifications:
cookies: dict[str, str] = {}
if headers.get("Cookie"):

62
tests/test_caido_api.py Normal file
View file

@ -0,0 +1,62 @@
"""Tests for request modification in the Caido proxy integration."""
from __future__ import annotations
from strix.tools.proxy.caido_api import apply_modifications, build_raw_request
def _components(body: str = "old", headers: dict[str, str] | None = None) -> dict:
return {
"method": "POST",
"url_path": "/submit",
"headers": headers or {"Content-Length": str(len(body))},
"body": body,
}
def test_apply_modifications_drops_stale_content_length_on_body_replace() -> None:
components = _components(body="old")
result = apply_modifications(
components,
{"body": "a much longer replacement body than the original"},
"http://example.com/submit",
)
assert "Content-Length" not in {k.title() for k in result["headers"]}
def test_apply_modifications_keeps_explicit_content_length_on_body_replace() -> None:
components = _components(body="old")
result = apply_modifications(
components,
{"body": "new-body", "headers": {"Content-Length": "8"}},
"http://example.com/submit",
)
assert result["headers"]["Content-Length"] == "8"
def test_apply_modifications_without_body_change_keeps_headers() -> None:
components = _components(body="old")
result = apply_modifications(
components,
{"headers": {"X-Test": "1"}},
"http://example.com/submit",
)
assert result["headers"]["Content-Length"] == "3"
def test_build_raw_request_recomputes_content_length_after_replacement() -> None:
components = _components(body="old")
new_body = "a much longer replacement body than the original"
modified = apply_modifications(
components,
{"body": new_body},
"http://example.com/submit",
)
_, raw = build_raw_request(
method=modified["method"],
url=modified["url"],
headers=modified["headers"],
body=modified["body"],
)
head = raw.decode("utf-8").split("\r\n\r\n", 1)[0]
assert f"Content-Length: {len(new_body.encode('utf-8'))}" in head