Skip to content

Commit 22dade4

Browse files
authored
Fix chunked read_body(length: 0) falsely completing the body (#691)
1 parent 3477fcc commit 22dade4

2 files changed

Lines changed: 39 additions & 7 deletions

File tree

lib/bandit/http1/socket.ex

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,16 +280,26 @@ defmodule Bandit.HTTP1.Socket do
280280
# do_read_chunked_data! reads up to the configured length, reading multiple
281281
# chunks to do so. It accumulates data in the 'body' list, adding to it
282282
# chunk by (possibly partial) chunk until either the end of the body is reached
283-
# or the configured length is exceeded
284-
@dialyzer {:no_improper_lists, do_read_chunked_data!: 5}
283+
# or the configured length is exceeded. Note that an exhausted read budget must
284+
# be checked before attempting to read a chunk, since a zero-byte chunk read is
285+
# otherwise indistinguishable from the terminal chunk
285286
defp do_read_chunked_data!(socket, buffer, body, body_length, opts) do
286287
max_to_read = Keyword.get(opts, :length, 8_000_000) - body_length
287288

289+
if max_to_read <= 0 do
290+
{:more, body, buffer}
291+
else
292+
do_read_next_chunk!(socket, buffer, body, body_length, max_to_read, opts)
293+
end
294+
end
295+
296+
@dialyzer {:no_improper_lists, do_read_next_chunk!: 6}
297+
defp do_read_next_chunk!(socket, buffer, body, body_length, max_to_read, opts) do
288298
case do_read_chunk!(socket, buffer, max_to_read, opts) do
289-
{<<>>, rest} ->
299+
{:done, rest} ->
290300
{:ok, body, rest}
291301

292-
{chunk, rest} ->
302+
{:chunk, chunk, rest} ->
293303
length = IO.iodata_length(chunk)
294304

295305
if length < max_to_read do
@@ -319,7 +329,7 @@ defmodule Bandit.HTTP1.Socket do
319329
if trailers != [],
320330
do: Logger.warning("Encountered trailers in chunked request; ignoring")
321331

322-
{<<>>, fake_socket.buffer}
332+
{:done, fake_socket.buffer}
323333

324334
{chunk_size, rest} ->
325335
to_read = min(chunk_size, max_to_read)
@@ -335,11 +345,11 @@ defmodule Bandit.HTTP1.Socket do
335345
if IO.iodata_to_binary(newline) != "\r\n",
336346
do: request_error!("Malformed chunked encoding request body")
337347

338-
{to_return, rest}
348+
{:chunk, to_return, rest}
339349

340350
remaining when remaining > 0 ->
341351
# Build a binary since we'll be passing it to read_chunk_size! which expects a binary
342-
{to_return, Integer.to_string(remaining, 16) <> "\r\n" <> rest}
352+
{:chunk, to_return, Integer.to_string(remaining, 16) <> "\r\n" <> rest}
343353
end
344354
end
345355
end

test/bandit/http1/protocol_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,28 @@ defmodule HTTP1ProtocolTest do
13461346
send_resp(conn, 200, "OK")
13471347
end
13481348

1349+
test "handles a read_body call with length: 0 without falsely completing the body", context do
1350+
client = SimpleHTTP1Client.tcp_client(context)
1351+
1352+
SimpleHTTP1Client.send(client, "POST", "/zero_length_chunked_read", [
1353+
"Host: localhost",
1354+
"Transfer-encoding: chunked"
1355+
])
1356+
1357+
Transport.send(client, "5\r\nhello\r\n0\r\n\r\n")
1358+
assert {:ok, "200 OK", _headers, "hello"} = SimpleHTTP1Client.recv_reply(client)
1359+
1360+
# Make sure the connection is still in sync by trying another request
1361+
SimpleHTTP1Client.send(client, "GET", "/echo_method", ["host: localhost"])
1362+
assert {:ok, "200 OK", _headers, "GET"} = SimpleHTTP1Client.recv_reply(client)
1363+
end
1364+
1365+
def zero_length_chunked_read(conn) do
1366+
{:more, "", conn} = Plug.Conn.read_body(conn, length: 0)
1367+
{:ok, body, conn} = Plug.Conn.read_body(conn)
1368+
send_resp(conn, 200, body)
1369+
end
1370+
13491371
test "reads a chunked body properly", context do
13501372
stream =
13511373
Stream.repeatedly(fn -> String.duplicate("0123456789", 100_000) end)

0 commit comments

Comments
 (0)