Refuse an oversized body before inviting it #413
@@ -32,6 +32,16 @@ heading with the date it merges, and tags `main` with the same date.
|
||||
and operator-specific values are replaced throughout. Commits keep their
|
||||
authors and dates, but a forge SHA no longer resolves on GitHub.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The credential proxy refuses an oversized request body before inviting
|
||||
it.** It answered `100 Continue` and only then read the declared length, so
|
||||
whether a session saw the `413` or only the `100` was timing. The cap itself
|
||||
always held - nothing oversized was read into host memory - but the
|
||||
conversation was a race, and it surfaced as an intermittently red `proxy`
|
||||
job.
|
||||
([#400](https://gitbox.branderson.io/dejima/dejima/issues/400))
|
||||
|
||||
## 2026.10.14
|
||||
|
||||
Nothing a session does has changed. This release is about where dejima can be
|
||||
|
||||
@@ -424,6 +424,33 @@ class Handler(BaseHTTPRequestHandler):
|
||||
% (what, n, self.MAX_BODY_BYTES))
|
||||
return n
|
||||
|
||||
def handle_expect_100(self):
|
||||
"""Refuse an oversized body before inviting it.
|
||||
|
||||
The default answers `100 Continue` and returns True, and it runs
|
||||
before do_* - so the proxy invited the upload, read the length in
|
||||
_read_body, refused it and closed mid-flight. Whether the client saw
|
||||
the 413 or only the 100 was timing, which is a race a client cannot
|
||||
be expected to win and a test cannot be expected to pass reliably.
|
||||
|
||||
The declared length is all there is to go on before a body arrives.
|
||||
A chunked request declares nothing and is capped as it is read, which
|
||||
is the one case where the conversation still ends mid-upload - there
|
||||
is no earlier moment to end it at.
|
||||
"""
|
||||
try:
|
||||
self._length(self.headers.get("Content-Length", 0), "Content-Length")
|
||||
except BodyTooLarge as e:
|
||||
# Closed for the same reason the other 413 closes: a client that
|
||||
# sends anyway - curl gives up waiting after a second - leaves its
|
||||
# bytes queued, and the next parse would read them as a request
|
||||
# line and answer with garbage 400s instead of one clean refusal.
|
||||
self.close_connection = True
|
||||
self._fail(413, "credential proxy: request body too large",
|
||||
log_message="refused a request body: %s" % e)
|
||||
return False
|
||||
return super().handle_expect_100()
|
||||
|
||||
def _read_body(self):
|
||||
if self.headers.get("Transfer-Encoding", "").lower() == "chunked":
|
||||
chunks = []
|
||||
|
||||
@@ -485,6 +485,45 @@ make_cert() {
|
||||
return 0
|
||||
}
|
||||
|
||||
@test "an oversized body is refused before the 100 Continue that invites it" {
|
||||
# The test above is about what the client ends up with, and it can pass
|
||||
# either way when the timing is kind: the proxy used to answer
|
||||
# `100 Continue` first and refuse from the body reader afterwards, so
|
||||
# whether a client read the 413 or only the 100 was a race. This one asks
|
||||
# the question the race cannot reach - what the FIRST answer is - and never
|
||||
# sends a body at all.
|
||||
start_proxy
|
||||
|
||||
run python3 - "$SOCK" "$PLACEHOLDER" <<'PROBE'
|
||||
import socket, sys
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect(sys.argv[1])
|
||||
# Declared far over the cap, and not one byte of it sent: a client offering a
|
||||
# body is entitled to be told no before it starts.
|
||||
sock.sendall(
|
||||
b"POST /v1/messages HTTP/1.1\r\n"
|
||||
b"Host: x\r\n"
|
||||
b"Authorization: Bearer " + sys.argv[2].encode() + b"\r\n"
|
||||
b"Content-Type: application/json\r\n"
|
||||
b"Content-Length: 99999999999\r\n"
|
||||
b"Expect: 100-continue\r\n\r\n")
|
||||
data = b""
|
||||
while b"\r\n" not in data:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
print(data.split(b"\r\n")[0].decode("ascii", "replace"))
|
||||
PROBE
|
||||
[ "$status" -eq 0 ] || { echo "the probe failed: $output"; return 1; }
|
||||
[[ "$output" == *"413"* ]] \
|
||||
|| { echo "the first answer was not a refusal: $output"; return 1; }
|
||||
grep -q 'refused a request body' "$TD/stderr" \
|
||||
|| { echo "nothing logged: $(cat "$TD/stderr")"; return 1; }
|
||||
}
|
||||
|
||||
@test "a normal-sized request body still passes through" {
|
||||
# The cap must be far above anything real. A conversation with attachments
|
||||
# is the biggest thing a Messages request carries.
|
||||
|
||||
Reference in New Issue
Block a user