#!/usr/bin/env bash set -o pipefail die(){ echo "$@" >&2 exit 1 } #RED='\033[0;31m' RED='\033[1;31m' YELLOW='\033[1;33m' GREEN='\033[1;32m' LBLUE='\033[1;34m' LCYAN='\033[1;36m' ORANGE='\033[0;33m' LGREY='\033[0;37m' BOLDJ='\033[1;37m' NC='\033[0m' # No Color stripAnsi(){ sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]//g" } trimWhitespace(){ sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } #wrapped echo wecho(){ builtin echo -e "$@" | fold -s -w 80 } drawInBox(){ innerWidth="45" echo -en "${LBLUE}╭" head -c $innerWidth /dev/zero | tr '\0' '-' echo -e "╮${NC}" while IFS= read -r line; do # The ansi characters mess up the string length so we need to strip them to calculate the width stripped="$(echo -n "$line" | stripAnsi)" leftPad=$(( ( innerWidth - ${#stripped} ) / 2)) rightPad=$(( ( innerWidth - leftPad ) - ${#stripped} )) if [ "${#stripped}" -gt "$innerWidth" ]; then line="$(echo -n "$line" | fold -w $((innerWidth - 5)) | head -n 1)..." stripped="$(echo -n "$line" | stripAnsi)" leftPad=$(( ( innerWidth - ${#stripped} ) / 2)) rightPad=$(( ( innerWidth - leftPad ) - ${#stripped} )) fi echo -en "${LBLUE}|${NC}" head -c $leftPad /dev/zero | tr '\0' ' ' echo -n "$line" head -c $rightPad /dev/zero | tr '\0' ' ' echo -e "${LBLUE}|${NC}" done echo -en "${LBLUE}╰" head -c $innerWidth /dev/zero | tr '\0' '-' echo -e "╯${NC}" } # gets the colour that should be output # 0 = green # 1 = yellow # 2 = red getColour(){ case "$1" in 0) echo -en "$GREEN" ;; 1) echo -en "$YELLOW" ;; 2) echo -en "$RED" ;; esac } printKey(){ echo -e "Not checked\ \t${GREEN}Fine${NC}\ \t${YELLOW}Mis-configured${NC}\ \t${RED}Missing${NC}" } generic_version_disclosure(){ local value local header value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)" header="$(echo "$1" | cut -d ':' -f 1 | trimWhitespace)" echo "$header" | drawInBox wecho -e "The server responds with ${ORANGE}$value${NC} in the \ $header header" wecho -e "This is potentially un-necesary information disclosure\n\n" [ -n "$value" ] && return 1 || return 0 } test_server(){ local value value="$(echo "$1" | cut -d ':' -f 2 | trimWhitespace)" echo "Server" | drawInBox wecho -e "The server responds with ${ORANGE}$value${NC} in the Server header" wecho -e "This is potentially un-necesary information disclosure\n\n" [ -n "$value" ] && return 1 || return 0 } test_x-powered-by(){ local value value="$(echo "$1" | cut -d ':' -f 2 | trimWhitespace)" echo "X-Powered-By" | drawInBox wecho -e "The server responds with ${ORANGE}$value${NC} in the X-Powered-By header" wecho -e "This is potentially un-necesary information disclosure\n\n" [ -n "$value" ] && return 1 || return 0 } test_x-xss-protection(){ local value value="$(echo "$1" | cut -d ':' -f 2 | grep -oE '[0-9]+' )" if [ "$value" = "0" ]; then return 0 else echo "X-XSS-Protection" | drawInBox wecho -e "The X-XSS-Protection header used to ask browsers to try and use \ internal heuristics to prevent reflected XSS attacks. It has been depreciated in all \ modern browsers that used to implement it. OWASP now suggests setting it to 0. https://owasp.org/www-project-secure-headers/#x-xss-protection\n\n" return 1 fi } test_x-frame-options(){ local value value="$(echo "$1" | cut -d ':' -f 2 | trimWhitespace | tr '[:lower:]' '[:upper:]')" case "$value" in "SAMEORIGIN"|"DENY") return 0 ;; "ALLOW-FROM"*) echo "X-Frame-Opitons" | drawInBox wecho -e "The ALLOW-FROM derivative is obsolete and no longer works \ in modern browsers.\n\n" wecho -e "The Content-Security-Policy HTTP header has a \ frame-ancestors directive which you can use instead.\n\n" return 1 ;; *) echo "X-Frame-Opitons" | drawInBox wecho "The X-Frame-Options HTTP response header can be used to \ indicate whether or not a browser should be allowed to render a page in a \ ,

If content is rendered above, the site is vulnerable to clickjacking

" wecho "To verify, type paste the following into your browser:" echo -e "\ndata:text/html;base64,$(echo "$source" | base64 -w 0)\n\n" return 2 esac } #test_x-content-type-options(){ #} test_content-security-policy(){ local value value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)" # TODO: work on content security testing local message="" if [ -z "$value" ]; then echo "Content-Security-Policy" | drawInBox wecho -e "The HTTP Content-Security-Policy response header allows web site \ administrators to control resources the user agent is allowed to load for a \ given page. With a few exceptions, policies mostly involve specifying server \ origins and script endpoints. This helps guard against cross-site scripting \ attacks (XSS).\n\n" return 2 else if echo "$value" | grep -q 'unsafe-inline'; then message+="The content security policy includes the \ ${ORANGE}unsafe-inline${NC} property which allows for inline JS/CSS assets. \ This prevents the content security policy from effectively mitigating against reflected or stored XSS attacks\n\n" elif echo "$value" | grep -q 'unsafe-eval'; then message+="The content security policy includes the \ ${ORANGE}unsafe-eval${NC} property which allows for eval to be used in JS. \ This prevents the content security policy from effectively mitigating against DOM based XSS attacks\n\n" fi # TODO, I'd like to check for more CSP issues. # See https://csp-evaluator.withgoogle.com/ # https://www.securing.pl/en/why-should-you-care-about-content-security-policy/ # https://lab.wallarm.com/how-to-trick-csp-in-letting-you-run-whatever-you-want-73cb5ff428aa/ fi if [ -n "$message" ]; then echo "Content-Security-Policy" | drawInBox message="$(echo "$message" | tr -d '\t')" wecho -e "$message" return 1 fi return 0 } test_strict-transport-security(){ local value local ret local output local maxAge value="$(echo "$1" | cut -d ':' -f 2 | trimWhitespace)" ret=0 output="" maxAge="$(echo "$value" | grep -oE 'max-age=[0-9]+' | grep -oE '[0-9]+')" if [ "$maxAge" -lt "31536000" ]; then output+="The max-age is set to a low value of ${ORANGE}$maxAge${NC}. We suggest setting it to at least 31536000.\n\n" ret=$((ret>1 ? ret : 1)) fi if ! echo "$value" | grep -q 'includeSubDomains'; then output+="The ${ORANGE}includeSubdomains${NC} property was not found. \ When included browsers won't connect to subdomains unless over an encrypted \ channel.\n\n" ret=$((ret>1 ? ret : 1)) fi #if ! echo "$value" | grep -q 'preload'; then # output+="The preload property " # ret=$((ret>1 ? ret : 1)) #fi if [ "$ret" -gt 0 ]; then echo "Strict-Transport-Security" | drawInBox wecho -e "$output" fi return $ret } test_set-cookie(){ local value local cookieName local ret local output value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)" cookieName="$(echo "$value" | cut -d '=' -f 1)" ret=0 output="" if ! echo "$value" | grep -q "HttpOnly"; then output+="The HttpOnly flag isn't set which means the cookie value can \ be read by JavaScript. If a malicious actor manages to run JavaScript through \ methods like XSS, they may be able to steal the contents of cookies\n\n" ret=$((ret>1 ? ret : 1)) fi if ! echo "$value" | grep -qi "Secure"; then output+="The Secure flag isn't set which means the cookie could be \ sent over unencrypted channels\n\n" ret=$((ret>1 ? ret : 1)) fi if ! echo "$value" | grep -q "SameSite=Strict"; then output+="The SameSite flag isn't set to Strict. The SameSite flag \ controls whether a cookie is sent with cross-origin requests, \ providing some protection against cross-site request forgery attacks. Strict means the browser sends the cookie only for same-site requests\n\n" ret=$((ret>1 ? ret : 1)) fi if echo "$value" | grep -iq "bigipserver"; then local ip_enc="$(echo "$value" | cut -d '=' -f 2 | cut -d '.' -f 1)" local port_enc="$(echo "$value" | cut -d '=' -f 2 | cut -d '.' -f 2)" local ip="$(echo "ibase=10;obase=16;$ip_enc"| bc | grep -o .. | tac | while read -r part; do echo -n "$((0x$part))."; done)" local port="$((0x$(echo "ibase=10;obase=16;$port_enc" | bc | grep -o .. | tac | tr -d '\n') ))" if echo "$ip" | grep -Eq '([0-9]{1,3}[\.]){3}[0-9]{1,3}'; then output+="The Cookie discloses internal IP addresses used by the load ballencer\n" output+="IP: $ip\n" output+="Port: $port\n\n" output+="Remediate this by enabling cookie encryption\n\ https://support.f5.com/csp/article/K7784?sr=14607726" ret=$((ret>1 ? ret : 1)) fi fi if [ "$ret" -gt 0 ]; then echo "Set-Cookie: $cookieName" | drawInBox wecho -e "$output" fi return "$ret" } test_permissions-policy(){ if [ -z "$1" ]; then echo "Permissions-Policy" | drawInBox wecho "The Permission-Policy header replaces the Feature-Policy and is \ used to allow or disallow certain browser features or apis in the interest of \ security.\n\n" return 2 fi } test_feature-policy(){ if [ -z "$1" ]; then echo "Feature-Policy" | drawInBox wecho "The Feature-Policy header was used to allow or disallow certian \ browser features or apis. It has been superceded by the permissions-policy header but should still be included for legacy browsers.\n\n" return 2 fi if ! echo "$headers" | grep -Eqi '^permissions-policy'; then echo "Feature-Policy" | drawInBox wecho "The Feature-Policy header was used to allow or disallow certian \ browser features or apis. It has been superceded by the permissions-policy header but should still be included for legacy browsers. It has been highlighted because the Permissions-policy header wasn't found.\n\n" return 2 fi } test_expect-ct(){ local value value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)" if [ -z "$1" ]; then echo "Expect-CT" | drawInBox wecho "When a site enables the Expect-CT header, they are requesting \ that the browser check that any certificate for that site appears in public \ CT logs. Initially, set the header without the enforce option but with report in order \ to check for potential breakages. The Expect-CT will likely become obsolete in June 2021. Since May 2018 new \ certificates are expected to support SCTs by default. Certificates before \ March 2018 were allowed to have a lifetime of 39 months, those will all be \ expired in June 2021.\n\n" return 2 elif ! echo "$value" | grep -q "enforce"; then echo "Expect-CT" | drawInBox wecho "The enforce directive was not found. It can be useful to omit \ this whilst testing the header, but should be added once testing has finished. Without the enforce directive, the browser will not refuse connections that \ violate the Certificate Transparency policy. The Expect-CT will likely become obsolete in June 2021. Since May 2018 new \ certificates are expected to support SCTs by default. Certificates before \ March 2018 were allowed to have a lifetime of 39 months, those will all be \ expired in June 2021.\n\n" return 1 fi } test_referer-policy-ct(){ local value value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)" if [ -z "$1" ]; then echo "Referrer-Policy" | drawInBox wecho "The Referrer-Policy HTTP header controls how much referrer \ information (sent via the Referer header) should be included with requests.\n\n" return 2 elif ! echo "$value" | grep -q "enforce"; then # TODO: add checks for different referer policy opitons return 1 fi } test_access-control-allow-origin(){ local value value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)" if [ "$value" = "*" ]; then echo "Access-Control-Allow-Origin" | drawInBox wecho "The Access-Control-Allow-Origin header indicates whether the \ response can be shared with requesting code from the given origin The value was found to be * meaning any origin. This is not normally desirable. \n" return 1 elif echo "$value" | grep -q "null"; then echo "Access-Control-Allow-Origin" | drawInBox wecho "The Access-Control-Allow-Origin header indicates whether the \ response can be shared with requesting code from the given origin The value was found to be null. the serialization of the Origin of any \ resource that uses a non-hierarchical scheme (such as data: or file: ) and \ sandboxed documents is defined to be \"null\". Many User Agents will grant \ such documents access to a response with an Access-Control-Allow-Origin: \ \"null\" header, and any origin can create a hostile document with a \"null\" \ Origin. The \"null\" value for the ACAO header should therefore be avoided.\n\n" return 1 fi return 0 } usage(){ echo -n "analyse-headers [OPTIONS]... URL Analyse the headers of a website Options: -h, --help Display this help and exit " } # Iterate over options breaking -ab into -a -b when needed and --foo=bar into # --foo bar optstring=h unset options while (($#)); do case $1 in # If option is of type -ab -[!-]?*) # Loop over each character starting with the second for ((i=1; i < ${#1}; i++)); do c=${1:i:1} # Add current char to options options+=("-$c") # If option takes a required argument, and it's not the last char make # the rest of the string its argument if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then options+=("${1:i+1}") break fi done ;; # If option is of type --foo=bar --?*=*) options+=("${1%%=*}" "${1#*=}") ;; # add --endopts for -- --) options+=(--endopts) ;; # Otherwise, nothing special *) options+=("$1") ;; esac shift done set -- "${options[@]}" unset options # Read the options and set stuff while [[ $1 = -?* ]]; do case $1 in -h|--help) usage; exit;; --) shift; break ;; *) die "invalid option: '$1'." ;; esac shift done # Store the remaining part as arguments. args+=("$@") url="${args[0]}" [ -z "$url" ] && die "You need to specify a url" # If url is -, read headers from stdin if [ "$url" = "-" ]; then headers="$(cat -)" else headers="$(curl -s -I "$url")" fi missingHeaders="x-frame-options content-security-policy x-xss-protection x-content-type-options feature-policy permissions-policy expect-ct" tmpfile="$(mktemp)" touch "$tmpfile" printKey echo "" echo "$headers" | sed -n '1p' while read -r line; do headerKey="$(echo "$line" | cut -d ':' -f1)" lowercase="$(echo "$headerKey" | tr '[:upper:]' '[:lower:]')" missingHeaders="$(echo -n "$missingHeaders" | sed '/'"$lowercase"'/d')" functionName="test_$lowercase" if declare -f "$functionName" > /dev/null; then "$functionName" "$line" >> "$tmpfile" colour="$(getColour "$?")" echo -e "${colour}$line${NC}" elif echo "$lowercase" | grep "version" > /dev/null; then # if the word version is in the line, assume version disclosure generic_version_disclosure "$line" >> "$tmpfile" colour="$(getColour "$?")" echo -e "${colour}$line${NC}" else echo "$line" fi done<<<"$(echo "$headers" | sed '1d')" # We don't want the initial http banner echo -n "$missingHeaders" | while read -r line; do echo -e "${RED}$line${NC}" functionName="test_$line" "$functionName" >> "$tmpfile" done echo "" cat "$tmpfile" rm "$tmpfile"