From c84e79f6d1ae735bfa191694ec6a62eb8231166d Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Mon, 10 Aug 2026 17:11:39 +0200 Subject: feat(http/ftp/smtp): implement max size checks fixes unbounded reads when using the "*l" pattern. Typically on headers and other control lines. --- test/maxsize_http.lua | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 test/maxsize_http.lua (limited to 'test/maxsize_http.lua') diff --git a/test/maxsize_http.lua b/test/maxsize_http.lua new file mode 100644 index 0000000..3c0dc74 --- /dev/null +++ b/test/maxsize_http.lua @@ -0,0 +1,110 @@ +-- Exercises the maxsize caps added to socket.http's line-based receive() +-- calls (see PLAN-RECEIVE-MAXSIZE.md). Self-contained: uses a single +-- process with a real TCP loopback connection, so it needs no paired +-- server script. +local socket = require "socket" +local http = require "socket.http" +local ltn12 = require "ltn12" + +local host = "127.0.0.1" + +-- connects `open_fn(host, port)` to a freshly bound loopback listener and +-- returns the client-side object it produced plus the server-side raw +-- socket accepted for that connection. +local function new_pair(open_fn) + local server = assert(socket.bind(host, 0)) + local ip, port = server:getsockname() + local client = assert(open_fn(ip, port)) + local srv = assert(server:accept()) + server:close() + return client, srv +end + +local failures = 0 + +local function check(ok, msg) + if ok then + print("PASS: " .. msg) + else + failures = failures + 1 + print("FAIL: " .. msg) + end +end + +local function http_open(ip, port) + return http.open(ip, port, socket.tcp) +end + +do -- sanity: normal status line + headers still parse + http.MAXHEADERLINE, http.MAXHEADERSIZE = 8192, 65536 + local h, srv = new_pair(http_open) + srv:send("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + local code = socket.protect(function() return h:receivestatusline() end)() + local headers = socket.protect(function() return h:receiveheaders() end)() + check(code == 200 and headers and headers["content-length"] == "0", + "http: normal status line + headers parse") + h:close(); srv:close() +end + +do -- status line over MAXHEADERLINE is rejected + http.MAXHEADERLINE, http.MAXHEADERSIZE = 16, 1024 + local h, srv = new_pair(http_open) + srv:send("HTTP/1.1 200 " .. string.rep("x", 40) .. "\r\n") + local code, err = socket.protect(function() return h:receivestatusline() end)() + check(code == nil and err == "oversized", + "http: status line over MAXHEADERLINE -> oversized") + h:close(); srv:close() +end + +do -- a single header line over MAXHEADERLINE is rejected + http.MAXHEADERLINE, http.MAXHEADERSIZE = 32, 1024 + local h, srv = new_pair(http_open) + srv:send("HTTP/1.1 200 OK\r\n") + assert(socket.protect(function() return h:receivestatusline() end)() == 200) + srv:send("X-Foo: " .. string.rep("y", 60) .. "\r\n\r\n") + local headers, err = socket.protect(function() return h:receiveheaders() end)() + check(headers == nil and err == "oversized", + "http: single header line over MAXHEADERLINE -> oversized") + h:close(); srv:close() +end + +do -- each header line individually fits MAXHEADERLINE, but the total exceeds MAXHEADERSIZE + http.MAXHEADERLINE, http.MAXHEADERSIZE = 32, 40 + local h, srv = new_pair(http_open) + srv:send("HTTP/1.1 200 OK\r\n") + assert(socket.protect(function() return h:receivestatusline() end)() == 200) + -- each header line is ~23 bytes, individually under MAXHEADERLINE(32) + srv:send("A: 111111111111111111\r\n") + srv:send("B: 222222222222222222\r\n") + local headers, err = socket.protect(function() return h:receiveheaders() end)() + check(headers == nil and err == "oversized", + "http: total headers over MAXHEADERSIZE -> oversized (no single line over MAXHEADERLINE)") + h:close(); srv:close() +end + +do -- chunk-size line over MAXHEADERLINE is rejected + http.MAXHEADERLINE, http.MAXHEADERSIZE = 32, 1024 + local h, srv = new_pair(http_open) + srv:send("HTTP/1.1 200 OK\r\n") + assert(socket.protect(function() return h:receivestatusline() end)() == 200) + srv:send("Transfer-Encoding: chunked\r\n\r\n") + local headers = assert(socket.protect(function() return h:receiveheaders() end)()) + srv:send(string.rep("f", 40) .. "\r\n") -- oversized chunk-size line + local t = {} + local ok, err = socket.protect(function() + return h:receivebody(headers, (ltn12.sink.table(t))) + end)() + check(ok == nil and err == "oversized", + "http: chunk-size line over MAXHEADERLINE -> oversized") + h:close(); srv:close() +end + +http.MAXHEADERLINE, http.MAXHEADERSIZE = 8192, 65536 + +if failures == 0 then + print("All http maxsize tests passed") + os.exit(0) +else + print(failures .. " http maxsize test(s) failed") + os.exit(1) +end -- cgit v1.2.3-55-g6feb