diff options
| author | Thijs Schreijer <thijs.schreijer@bookingexperts.com> | 2026-08-10 17:11:39 +0200 |
|---|---|---|
| committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2026-08-29 17:42:13 +0200 |
| commit | c84e79f6d1ae735bfa191694ec6a62eb8231166d (patch) | |
| tree | 27e8efe3a7bd838b655eb05307a2e730ff16cd8e /src/http.lua | |
| parent | 52f9f91d91aa33a1483334f69073f3e6250cd168 (diff) | |
| download | luasocket-c84e79f6d1ae735bfa191694ec6a62eb8231166d.tar.gz luasocket-c84e79f6d1ae735bfa191694ec6a62eb8231166d.tar.bz2 luasocket-c84e79f6d1ae735bfa191694ec6a62eb8231166d.zip | |
feat(http/ftp/smtp): implement max size checks
fixes unbounded reads when using the "*l" pattern. Typically on
headers and other control lines.
Diffstat (limited to 'src/http.lua')
| -rw-r--r-- | src/http.lua | 27 |
1 files changed, 21 insertions, 6 deletions
diff --git a/src/http.lua b/src/http.lua index 2c296d4..f031c1d 100644 --- a/src/http.lua +++ b/src/http.lua | |||
| @@ -25,6 +25,11 @@ local _M = socket.http | |||
| 25 | _M.TIMEOUT = 60 | 25 | _M.TIMEOUT = 60 |
| 26 | -- user agent field sent in request | 26 | -- user agent field sent in request |
| 27 | _M.USERAGENT = socket._VERSION | 27 | _M.USERAGENT = socket._VERSION |
| 28 | -- maximum size of a single header line (also used for the status line and | ||
| 29 | -- the chunk-size line, which carry the same shape of risk) | ||
| 30 | _M.MAXHEADERLINE = 8192 | ||
| 31 | -- maximum total size of all header lines in a single header block | ||
| 32 | _M.MAXHEADERSIZE = 65536 | ||
| 28 | 33 | ||
| 29 | -- supported schemes and their particulars | 34 | -- supported schemes and their particulars |
| 30 | local SCHEMES = { | 35 | local SCHEMES = { |
| @@ -47,8 +52,18 @@ local SCHEMES = { | |||
| 47 | local function receiveheaders(sock, headers) | 52 | local function receiveheaders(sock, headers) |
| 48 | local line, name, value, err | 53 | local line, name, value, err |
| 49 | headers = headers or {} | 54 | headers = headers or {} |
| 55 | -- bounds total bytes read across all header lines, on top of the | ||
| 56 | -- per-line MAXHEADERLINE cap, so a peer can't exhaust memory by sending | ||
| 57 | -- many lines that each individually fit under MAXHEADERLINE | ||
| 58 | local budget = _M.MAXHEADERSIZE | ||
| 59 | local function recvline() | ||
| 60 | if budget <= 0 then return nil, "oversized" end | ||
| 61 | local line, err = sock:receive("*l", nil, math.min(budget, _M.MAXHEADERLINE)) | ||
| 62 | if line then budget = budget - #line end | ||
| 63 | return line, err | ||
| 64 | end | ||
| 50 | -- get first line | 65 | -- get first line |
| 51 | line, err = sock:receive("*l") | 66 | line, err = recvline() |
| 52 | if err then return nil, err end | 67 | if err then return nil, err end |
| 53 | -- headers go until a blank line is found | 68 | -- headers go until a blank line is found |
| 54 | while line ~= "" do | 69 | while line ~= "" do |
| @@ -57,12 +72,12 @@ local function receiveheaders(sock, headers) | |||
| 57 | if not (name and value) then return nil, "malformed response headers" end | 72 | if not (name and value) then return nil, "malformed response headers" end |
| 58 | name = string.lower(name) | 73 | name = string.lower(name) |
| 59 | -- get next line (value might be folded) | 74 | -- get next line (value might be folded) |
| 60 | line, err = sock:receive("*l") | 75 | line, err = recvline() |
| 61 | if err then return nil, err end | 76 | if err then return nil, err end |
| 62 | -- unfold any folded values | 77 | -- unfold any folded values |
| 63 | while string.find(line, "^%s") do | 78 | while string.find(line, "^%s") do |
| 64 | value = value .. line | 79 | value = value .. line |
| 65 | line, err = sock:receive("*l") | 80 | line, err = recvline() |
| 66 | if err then return nil, err end | 81 | if err then return nil, err end |
| 67 | end | 82 | end |
| 68 | -- save pair in table | 83 | -- save pair in table |
| @@ -82,7 +97,7 @@ socket.sourcet["http-chunked"] = function(sock, headers) | |||
| 82 | }, { | 97 | }, { |
| 83 | __call = function() | 98 | __call = function() |
| 84 | -- get chunk size, skip extension | 99 | -- get chunk size, skip extension |
| 85 | local line, err = sock:receive("*l") | 100 | local line, err = sock:receive("*l", nil, _M.MAXHEADERLINE) |
| 86 | if err then return nil, err end | 101 | if err then return nil, err end |
| 87 | local size = base.tonumber(string.gsub(line, ";.*", ""), 16) | 102 | local size = base.tonumber(string.gsub(line, ";.*", ""), 16) |
| 88 | if not size then return nil, "invalid chunk size" end | 103 | if not size then return nil, "invalid chunk size" end |
| @@ -90,7 +105,7 @@ socket.sourcet["http-chunked"] = function(sock, headers) | |||
| 90 | if size > 0 then | 105 | if size > 0 then |
| 91 | -- if not, get chunk and skip terminating CRLF | 106 | -- if not, get chunk and skip terminating CRLF |
| 92 | local chunk, err, _ = sock:receive(size) | 107 | local chunk, err, _ = sock:receive(size) |
| 93 | if chunk then sock:receive("*l") end | 108 | if chunk then sock:receive("*l", nil, _M.MAXHEADERLINE) end |
| 94 | return chunk, err | 109 | return chunk, err |
| 95 | else | 110 | else |
| 96 | -- if it was, read trailers into headers table | 111 | -- if it was, read trailers into headers table |
| @@ -167,7 +182,7 @@ function metat.__index:receivestatusline() | |||
| 167 | return nil, status | 182 | return nil, status |
| 168 | end | 183 | end |
| 169 | -- otherwise proceed reading a status line | 184 | -- otherwise proceed reading a status line |
| 170 | status = self.try(self.c:receive("*l", status)) | 185 | status = self.try(self.c:receive("*l", status, _M.MAXHEADERLINE)) |
| 171 | local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) | 186 | local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) |
| 172 | return self.try(base.tonumber(code), status) | 187 | return self.try(base.tonumber(code), status) |
| 173 | end | 188 | end |
