mirror of
https://github.com/usestrix/strix.git
synced 2026-08-22 19:09:37 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e03133eddd | ||
|
|
0b45d223c3 |
@@ -167,9 +167,6 @@ async def get_request_with_client(
|
||||
return await client.request.get(request_id, opts)
|
||||
|
||||
|
||||
_FRAMING_HEADERS = frozenset({"content-length", "transfer-encoding"})
|
||||
|
||||
|
||||
def build_raw_request(
|
||||
*,
|
||||
method: str,
|
||||
@@ -190,16 +187,7 @@ def build_raw_request(
|
||||
final_headers = {**headers}
|
||||
final_headers.setdefault("Host", parsed.netloc)
|
||||
final_headers.setdefault("User-Agent", "strix")
|
||||
# Framing headers inherited from the captured request describe the ORIGINAL
|
||||
# body; once the body is modified for replay they are stale. We always send a
|
||||
# plain (non-chunked) body with an explicit Content-Length, so drop any
|
||||
# inherited Content-Length AND Transfer-Encoding (case-insensitively) and
|
||||
# recompute the length from the body actually being sent. This keeps the two
|
||||
# framing mechanisms from conflicting (RFC 7230 3.3.3: a leftover
|
||||
# Transfer-Encoding would make the target ignore Content-Length and try to
|
||||
# parse the body as chunked), so the replay is never desynced.
|
||||
final_headers = {k: v for k, v in final_headers.items() if k.lower() not in _FRAMING_HEADERS}
|
||||
if body:
|
||||
if body and "Content-Length" not in {k.title() for k in final_headers}:
|
||||
final_headers["Content-Length"] = str(len(body.encode("utf-8")))
|
||||
|
||||
lines = [f"{method.upper()} {path} HTTP/1.1"]
|
||||
|
||||
@@ -140,59 +140,6 @@ 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_transfer_encoding_for_modified_body() -> None:
|
||||
body = '{"user":"updated"}'
|
||||
_conn, raw = caido_api.build_raw_request(
|
||||
method="POST",
|
||||
url="https://example.com/login",
|
||||
headers={
|
||||
"tRaNsFeR-EnCoDiNg": "chunked",
|
||||
"Content-Length": "7",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body=body,
|
||||
)
|
||||
assert _headers_named(raw, "Transfer-Encoding") == []
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user