From 5e2bb7ad860598e5c81a90aa7bc2f0d4d74f78a5 Mon Sep 17 00:00:00 2001 From: dmiller Date: Mon, 2 Nov 2015 16:02:50 +0000 Subject: [PATCH] Fix parsing of Set-Cookie headers. Closes #229 --- CHANGELOG | 3 + nselib/http.lua | 150 ++++++++++++++++++++---------------------------- 2 files changed, 64 insertions(+), 89 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0feded5d9..225def7b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] [GH#229] Improve parsing in http.lua for multiple Set-Cookie headers in + a single response. [nnposter] + o [NSE] [GH#194] Add support for reading fragmented TLS messages to ssl-enum-ciphers. [Jacob Gajek] diff --git a/nselib/http.lua b/nselib/http.lua index afa209c52..08e89422a 100644 --- a/nselib/http.lua +++ b/nselib/http.lua @@ -651,6 +651,8 @@ local function parse_status_line(status_line, response) return true end +local parse_set_cookie -- defined farther down + -- Sets response.header and response.rawheader. local function parse_header(header, response) local pos @@ -684,10 +686,22 @@ local function parse_header(header, response) -- Set it in our table. name = string.lower(name) + local value = table.concat(words, " ") if response.header[name] then - response.header[name] = response.header[name] .. ", " .. table.concat(words, " ") + -- TODO: delay concatenation until return to avoid resource exhaustion + response.header[name] = response.header[name] .. ", " .. value else - response.header[name] = table.concat(words, " ") + response.header[name] = value + end + + -- Update the cookie table if this is a Set-Cookie header + if name == "set-cookie" then + local cookie, err = parse_set_cookie(value) + if cookie then + response.cookies[#response.cookies + 1] = cookie + else + -- Ignore any cookie parsing error + end end -- Next field, or end of string. (If not it's an error.) @@ -701,8 +715,8 @@ local function parse_header(header, response) return true end --- Parse the contents of a Set-Cookie header field. The result is an array --- containing tables of the form +-- Parse the contents of a Set-Cookie header field. +-- The result is a table of the form -- -- { name = "NAME", value = "VALUE", Comment = "...", Domain = "...", ... } -- @@ -713,96 +727,64 @@ end -- "HISTORICAL". Values need not be quoted, but if they start with a quote they -- will be interpreted as a quoted string. local function parse_set_cookie(s) - local cookies local name, value - local _, pos + local _ - cookies = {} + local cookie = {} - pos = 1 - while true do - local cookie = {} + -- Get the NAME=VALUE part. + local pos = skip_space(s, 1) + pos, cookie.name = get_token(s, pos) + if not cookie.name then + return nil, "Can't get cookie name." + end + pos = skip_space(s, pos) + if s:sub(pos, pos) ~= "=" then + return nil, string.format("Expected '=' after cookie name \"%s\".", cookie.name) + end + pos = pos + 1 + pos = skip_space(s, pos) + if s:sub(pos, pos) == "\"" then + pos, cookie.value = get_quoted_string(s, pos) + else + _, pos, cookie.value = s:find("([^; \t]*)", pos) + pos = pos + 1 + end + if not cookie.value then + return nil, string.format("Can't get value of cookie named \"%s\".", cookie.name) + end + pos = skip_space(s, pos) - -- Get the NAME=VALUE part. - pos = skip_space(s, pos) - pos, cookie.name = get_token(s, pos) - if not cookie.name then - return nil, "Can't get cookie name." - end - pos = skip_space(s, pos) - if pos > #s or string.sub(s, pos, pos) ~= "=" then - return nil, string.format("Expected '=' after cookie name \"%s\".", cookie.name) - end + -- Loop over the attributes. + while s:sub(pos, pos) == ";" do pos = pos + 1 pos = skip_space(s, pos) - if string.sub(s, pos, pos) == "\"" then - pos, cookie.value = get_quoted_string(s, pos) - else - _, pos, cookie.value = string.find(s, "([^;]*)[ \t]*", pos) - pos = pos + 1 - end - if not cookie.value then - return nil, string.format("Can't get value of cookie named \"%s\".", cookie.name) + pos, name = get_token(s, pos) + if not name then + return nil, string.format("Can't get attribute name of cookie \"%s\".", cookie.name) end pos = skip_space(s, pos) - - -- Loop over the attributes. - while pos <= #s and string.sub(s, pos, pos) == ";" do + if s:sub(pos, pos) == "=" then pos = pos + 1 pos = skip_space(s, pos) - pos, name = get_token(s, pos) - if not name then - return nil, string.format("Can't get attribute name of cookie \"%s\".", cookie.name) - end - pos = skip_space(s, pos) - if pos <= #s and string.sub(s, pos, pos) == "=" then - pos = pos + 1 - pos = skip_space(s, pos) - if string.sub(s, pos, pos) == "\"" then - pos, value = get_quoted_string(s, pos) - else - -- account for the possibility of the expires attribute being empty or improperly formatted - local last_pos = pos - - if string.lower(name) == "expires" then - -- For version 0 cookies we must allow one comma for "expires". - _, pos, value = string.find(s, "([^,]*,[^;,]*)[ \t]*", pos) - else - _, pos, value = string.find(s, "([^;,]*)[ \t]*", pos) - end - - -- account for the possibility of the expires attribute being empty or improperly formatted - if ( not(pos) ) then - _, pos, value = s:find("([^;]*)", last_pos) - end - - pos = pos + 1 - end - if not value then - return nil, string.format("Can't get value of cookie attribute \"%s\".", name) - end + if s:sub(pos, pos) == "\"" then + pos, value = get_quoted_string(s, pos) else - value = true + _, pos, value = s:find("([^;]*)", pos) + value = value:match("(.-)[ \t]*$") + pos = pos + 1 end - cookie[name:lower()] = value - pos = skip_space(s, pos) + if not value then + return nil, string.format("Can't get value of cookie attribute \"%s\".", name) + end + else + value = true end - - cookies[#cookies + 1] = cookie - - if pos > #s then - break - end - - if string.sub(s, pos, pos) ~= "," then - return nil, string.format("Syntax error after cookie named \"%s\".", cookie.name) - end - - pos = pos + 1 + cookie[name:lower()] = value pos = skip_space(s, pos) end - return cookies + return cookie end -- Read one response from the socket s and return it after @@ -818,6 +800,7 @@ local function next_response(s, method, partial) ["status-line"]=nil, header={}, rawheader={}, + cookies={}, body="" } @@ -845,17 +828,6 @@ local function next_response(s, method, partial) end response.body = body - -- We have the Status-Line, header, and body; now do any postprocessing. - - response.cookies = {} - if response.header["set-cookie"] then - response.cookies, err = parse_set_cookie(response.header["set-cookie"]) - if not response.cookies then - -- Ignore a cookie parsing error. - response.cookies = {} - end - end - return response, partial end