diff --git a/nselib/http.lua b/nselib/http.lua index 1f1a12380..2a38f5d6b 100644 --- a/nselib/http.lua +++ b/nselib/http.lua @@ -75,8 +75,8 @@ -- * bypass_cache: Do not perform a lookup in the local HTTP cache. -- * no_cache: Do not save the result of this request to the local HTTP cache. -- * no_cache_body: Do not save the body of the response to the local HTTP cache. --- * redirect_ok: Closure that overrides the default redirect_ok used to validate whether to follow HTTP redirects or not. False, if no HTTP redirects should be followed. --- The following example shows how to write a custom closure that follows 5 consecutive redirects: +-- * redirect_ok: Closure that overrides the default redirect_ok used to validate whether to follow HTTP redirects or not. False, if no HTTP redirects should be followed. Alternatively, a number may be passed to change the number of redirects to follow. +-- The following example shows how to write a custom closure that follows 5 consecutive redirects, without the safety checks in the default redirect_ok: -- -- redirect_ok = function(host,port) -- local c = 5 @@ -343,8 +343,8 @@ local function validate_options(options) bad = true end elseif(key == 'redirect_ok') then - if(type(value)~= 'function' and type(value)~='boolean') then - stdnse.debug1("http: options.redirect_ok must be a function or boolean") + if(type(value)~= 'function' and type(value)~='boolean' and type(value) ~= 'number') then + stdnse.debug1("http: options.redirect_ok must be a function or boolean or number") bad = true end else @@ -1303,75 +1303,81 @@ function put(host, port, path, options, putdata) return generic_request(host, port, "PUT", path, mod_options) end --- Check if the given URL is okay to redirect to. Return a table with keys +-- A battery of tests a URL is subjected to in order to decide if it may be +-- redirected to. They incrementally fill in loc.host, loc.port, and loc.path. +local redirect_ok_rules = { + + -- Check if there's any credentials in the url + function (url, host, port) + -- bail if userinfo is present + return ( url.userinfo and false ) or true + end, + + -- Check if the location is within the domain or host + function (url, host, port) + local hostname = stdnse.get_hostname(host) + if ( hostname == host.ip and host.ip == url.host.ip ) then + return true + end + local domain = hostname:match("^[^%.]-%.(.*)") or hostname + local match = ("^.*%s$"):format(domain) + if ( url.host:match(match) ) then + return true + end + return false + end, + + -- Check whether the new location has the same port number + function (url, host, port) + -- port fixup, adds default ports 80 and 443 in case no url.port was + -- defined, we do this based on the url scheme + local url_port = url.port + if ( not(url_port) ) then + if ( url.scheme == "http" ) then + url_port = 80 + elseif( url.scheme == "https" ) then + url_port = 443 + end + end + if (not url_port) or tonumber(url_port) == port.number then + return true + end + return false + end, + + -- Check whether the url.scheme matches the port.service + function (url, host, port) + -- if url.scheme is present then it must match the scanned port + if url.scheme and url.port then return true end + if url.scheme and url.scheme ~= port.service then return false end + return true + end, + + -- make sure we're actually being redirected somewhere and not to the same url + function (url, host, port) + -- path cannot be unchanged unless host has changed + -- loc.path must be set if returning true + if ( not url.path or url.path == "/" ) and url.host == ( host.targetname or host.ip) then return false end + if not url.path then return true end + return true + end, +} + +--- Check if the given URL is okay to redirect to. Return a table with keys -- "host", "port", and "path" if okay, nil otherwise. +-- +-- Redirects will be followed unless they: +-- * contain credentials +-- * are on a different domain or host +-- * have a different port number or URI scheme +-- * redirect to the same URI +-- * exceed the maximum number of redirects specified -- @param url table as returned by url.parse -- @param host table as received by the action function -- @param port table as received by the action function +-- @param counter number of redirects to follow. -- @return loc table containing the new location -function redirect_ok(host, port) - - -- A battery of tests a URL is subjected to in order to decide if it may be - -- redirected to. They incrementally fill in loc.host, loc.port, and loc.path. - local rules = { - - -- Check if there's any credentials in the url - function (url, host, port) - -- bail if userinfo is present - return ( url.userinfo and false ) or true - end, - - -- Check if the location is within the domain or host - function (url, host, port) - local hostname = stdnse.get_hostname(host) - if ( hostname == host.ip and host.ip == url.host.ip ) then - return true - end - local domain = hostname:match("^[^%.]-%.(.*)") or hostname - local match = ("^.*%s$"):format(domain) - if ( url.host:match(match) ) then - return true - end - return false - end, - - -- Check whether the new location has the same port number - function (url, host, port) - -- port fixup, adds default ports 80 and 443 in case no url.port was - -- defined, we do this based on the url scheme - local url_port = url.port - if ( not(url_port) ) then - if ( url.scheme == "http" ) then - url_port = 80 - elseif( url.scheme == "https" ) then - url_port = 443 - end - end - if (not url_port) or tonumber(url_port) == port.number then - return true - end - return false - end, - - -- Check whether the url.scheme matches the port.service - function (url, host, port) - -- if url.scheme is present then it must match the scanned port - if url.scheme and url.port then return true end - if url.scheme and url.scheme ~= port.service then return false end - return true - end, - - -- make sure we're actually being redirected somewhere and not to the same url - function (url, host, port) - -- path cannot be unchanged unless host has changed - -- loc.path must be set if returning true - if ( not url.path or url.path == "/" ) and url.host == ( host.targetname or host.ip) then return false end - if not url.path then return true end - return true - end, - } - - local counter = MAX_REDIRECT_COUNT +function redirect_ok(host, port, counter) -- convert a numeric port to a table if ( "number" == type(port) ) then port = { number = port } @@ -1379,7 +1385,7 @@ function redirect_ok(host, port) return function(url) if ( counter == 0 ) then return false end counter = counter - 1 - for i, rule in ipairs( rules ) do + for i, rule in ipairs( redirect_ok_rules ) do if ( not(rule( url, host, port )) ) then --stdnse.debug1("Rule failed: %d", i) return false @@ -1435,11 +1441,13 @@ local function get_redirect_ok(host, port, options) return function() return false end elseif( "function" == type(options.redirect_ok) ) then return options.redirect_ok(host, port) + elseif( type(options.redirect_ok) == "number") then + return redirect_ok(host, port, options.redirect_ok) else - return redirect_ok(host, port) + return redirect_ok(host, port, MAX_REDIRECT_COUNT) end else - return redirect_ok(host, port) + return redirect_ok(host, port, MAX_REDIRECT_COUNT) end end