NSE LIB

Back to library
Unofficial safe Default

http-sec-headers

Makes a request to the root folder ("/") of a web server and reports on the security headers that are missing from the data. This script mimics the functionality of https://securityheaders.io and is modeled after http-headers.nse.

Ports

Any

Protocols

n/a

Attribution

Jeffrey Stiles (upstream: aerissecure/nse)

Usage

Copy the command and adjust the target or script arguments as needed.

nmap --script http-sec-headers <target>
Script Source Toggle

The full script source is stored with this entry and is hidden by default to keep the page easier to scan.

description = [[
Makes a request to the root folder ("/") of a web server and reports on the security headers that are missing from the data. This script mimics the functionality of https://securityheaders.io and is modeled after http-headers.nse.
]]

---
-- @args http-sec-headers.username Basic auth username
-- @args http-sec-headers.password Basic auth password
-- @args http-sec-headers.url-path The path to request. Defaults to <code>/</code>.
--
-- @usage
-- nmap --script http-sec-headers <target>
--
-- @output
-- 443/tcp open  https   syn-ack
-- | http-sec-headers:
-- |   missing:
-- |     Content-Security-Policy
-- |     Permissions-Policy
-- |     Expect-CT
-- |   present:
-- |     X-XSS-Protection: 1; mode=block
-- |     X-Frame-Options: SAMEORIGIN
-- |     X-Content-Type-Options: nosniff
-- |     Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-- |     Referrer-Policy: strict-origin
-- |   hostname: example.com
-- |_  status: 200
-- @xmloutput
-- <table key="missing">
-- <elem>Content-Security-Policy</elem>
-- <elem>Permissions-Policy</elem>
-- <elem>Expect-CT</elem>
-- </table>
-- <table key="present">
-- <elem key="Referrer-Policy">strict-origin</elem>
-- <elem key="Strict-Transport-Security">max-age=31536000; includeSubDomains; preload</elem>
-- <elem key="X-XSS-Protection">1; mode=block</elem>
-- <elem key="X-Content-Type-Options">nosniff</elem>
-- <elem key="X-Frame-Options">SAMEORIGIN</elem>
-- </table>
-- <elem key="hostname">example.com</elem>
-- <elem key="status">200</elem>


-- HTTP Security Headers
-- rev 2.0 (2018-02-06)
-- Original NASL script by Jeffrey Stiles (@uthcr33p)(jeff@aerissecure.com)


categories = {"default", "discovery", "safe", "vuln"}
author = "Jeffrey Stiles"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

local http = require "http"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"


portrule = shortport.http

---Get the best possible hostname for the given host. Like stdnse.get_hostname
-- but does not use reverse dns name.
local function get_hostname(host)
  if type(host) == "table" then
    return host.targetname or host.ip
  else
    return host
  end
end

action = function(host, port)
    local path = "/"
    local method = "GET"
    local https_redirect = false
    local redirect_location = nil
    local hostname = get_hostname(host)
    local auth = nil

    local username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
    local password = stdnse.get_script_args(SCRIPT_NAME .. ".password")
    local argpath = stdnse.get_script_args(SCRIPT_NAME .. ".url-path")
    if argpath ~= nil then
        path = argpath
    end


    if username and password then
        auth = {username=username, password=password}
    end

    stdnse.verbose("Sending %s request to %s:%s", method, hostname, port.number)
    response = http.generic_request(hostname, port, method, path, {auth=auth})

    -- validate response
    if response == nil then
        return stdnse.format_output(false, "Request returned an empty response")
    end
    if response.rawheader == nil then
        return stdnse.format_output(false, "Request returned an empty rawheader table")
    end

    stdnse.verbose("Response status: %s (ssl=%s)", response.status, response.ssl)

    local code = tostring(response.status)

    -- check for http -> http redirect
    local redirect = not(code:match("^30[012378]$") == nil)
    if (response.ssl == false and redirect) then
        stdnse.verbose("redirect detected. target: "..response.header.location)
        -- response.header.location only exists for redirects
        if response.header.location:sub(1, #"https") == "https" then
            https_redirect = true
        end
    end

    local output = stdnse.output_table()
    output.missing = {}
    output.present = {}
    output.hostname = hostname
    if path ~= "/" then
        output.path = path
    end
    if not response.ssl then
        output["redirect-http-to-https"] = https_redirect
    end
    if redirect then
        output["redirect-location"] = response.header.location
    end

    output.status = code
    if not(code:match("^[45]") == nil) then
        output.status = output.status .. " (possible error)"
    end

    -- restrict assets the browser can load
    local hdrval = response.header['content-security-policy']
    if hdrval == nil then
        table.insert(output.missing, "Content-Security-Policy")
    else
        output.present["Content-Security-Policy"] = hdrval
    end

    -- only supports one value: nosniff
    hdrval = response.header['x-content-type-options']
    if hdrval == nil then
        table.insert(output.missing, "X-Content-Type-Options")
    else
        output.present["X-Content-Type-Options"] = hdrval
    end

    -- prevent click-jacking. Values include DENY, SAMEORIGIN, ALLOW-FROM
    hdrval = response.header['x-frame-options']
    if hdrval == nil then
        table.insert(output.missing, "X-Frame-Options")
    else
        output.present["X-Frame-Options"] = hdrval
    end

    -- recommended value is "1" (enabled) and "mode=block" (instead of "=report")
    hdrval = response.header['x-xss-protection']
    if hdrval == nil then
        table.insert(output.missing, "X-XSS-Protection")
    else
        output.present["X-XSS-Protection"] = hdrval
    end

    -- controls information leaked in the referer header
    hdrval = response.header['referrer-policy']
    if hdrval == nil then
        table.insert(output.missing, "Referrer-Policy")
    else
        output.present["Referrer-Policy"] = hdrval
    end

    hdrval = response.header['permissions-policy']
    if hdrval == nil then
        table.insert(output.missing, "Permissions-Policy")
    else
        output.present["Permissions-Policy"] = hdrval
    end

    --  minimum recommended value is 2592000 (30 days).
    hdrval = response.header['strict-transport-security']
    if response.ssl and hdrval == nil then
        table.insert(output.missing, "Strict-Transport-Security")
    else
        output.present["Strict-Transport-Security"] = hdrval
    end

    hdrval = response.header['expect-ct']
    if response.ssl and hdrval == nil then
        table.insert(output.missing, "Expect-CT")
    else
        output.present["Expect-CT"] = hdrval
    end


    hdrval = response.header['content-type']
    if hdrval ~= nil then
        stdnse.verbose("Response Content-Type: "..response.header['content-type'])
    end

    -- remove empty sections
    if next(output.missing) == nil then
        output.missing = nil
    end

    if next(output.present) == nil then
        output.present = nil
    end

    return output

end

Overview

Imported from the upstream repository aerissecure/nse. Makes a request to the root folder (”/”) of a web server and reports on the security headers that are missing from the data. This script mimics the functionality of https://securityheaders.io and is modeled after http-headers.nse.