You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

608 lines
18 KiB

#!/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 \
<frame>, <iframe>, <embed> or <object>. Sites can use this to avoid \
click-jacking attacks, by ensuring that their content is not embedded into \
other sites."
if echo "$headers" |
grep -Eqi '^content-security-policy:.*frame-ancestors.*'; then
wecho "It looks like the content security policy contains the \
frame ancestors directive. This also mitigates against the clickjacking \
although browser support isn't as strong meaning you should still include the \
x-frame-options header"
fi
source="
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width' />
<title>Clickjacking example</title>
<style type='text/css' media='screen'>
body{
width: 100vw;
height: 100vh;
border: 2px solid black;
}
iframe{
border: 3px solid black;
width: 80%;
height: 80%;
margin: 20px auto;
display: block;
}
h1, p{
text-align: center;
}
</style>
</head>
<body>
<h1>Clickjacking example</h1>
<iframe src='$url'>
</iframe>
<p>If content is rendered above, the site is vulnerable to clickjacking</p>
</body>
</html>
"
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
local ret=0
value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)"
# TODO: work on content security testing
local message=""
if [ -z "$value" ]; then
message+="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"
ret=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"
ret=$((ret>1 ? ret : 1))
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"
ret=$((ret>1 ? ret : 1))
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
message+="The content security policy should be carefully considered \
before implementing as mis-configuring it can lead to site breakages. Scripts \
and stylesheets should be sourced from a carefully curated list of trusted \
domains that do now allow user uploaded content. Some CDNs should also be \
avoided if they host outdated versions of libraries that are known to be \
vulnerable or JSONP content, as both of these can lead to Cross Site Scripting \
(XSS). In order to prevent other types of XSS attack, unsafe-inline and \
unsafe-eval sources should be avoided in favour of putting scripts / styles in \
external resources or, if that is not possible, whitelisted inline scripts / \
styles using <hash-algorithm>-<hash> sources.
In order to prevent use of plugins such as flash and silverlight, use the \
{code}object-src 'none'{/code} directive.
In order to prevent framing, use the {code}frame-ancestors 'none'{/code} \
directive.
The recomended header for APIs is
{code}
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
{/code}
Which disables loading of all sub-resources and stops the API response being
framed.
There is also a related content-security-policy-report-only header that will \
not enforce rules, but will report violations. This is useful for testing \
purposes
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy\n\n"
echo "Content-Security-Policy" | drawInBox
message="$(echo "$message" | tr -d '\t')"
wecho -e "$message"
return "$ret"
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=""
if [ -z "$value" ]; then
output+="The HTTP Strict Transport Security response header intructs \
browsers to only connect to it via an encrypted channel.\n\n"
ret=2
else
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
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
}
test_cache-control(){
local value
value="$(echo "$1" | cut -d ':' -f 2- | trimWhitespace)"
if [ -z "$1" ] || ! echo "$value" | grep -q "no-store"; then
echo "Cache-Control" | drawInBox
wecho "The Cache-Control header instructs the browser if and for how \
long browsers may cache responses. If responses contain sensitive information, \
they should not be cached. In order to enforce this, add the no-store directive.\n"
echo -e "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\n\n"
[ -z "$1" ] && return 2 || return 1
fi
}
usage(){
echo -n "analyse-headers [OPTIONS]... URL
Analyse the headers of a website
Options:
-h, --help Display this help and exit
-k, --insecure Ignores certificate errors
"
}
# 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
insecure=""
# Read the options and set stuff
while [[ $1 = -?* ]]; do
case $1 in
-h|--help) usage; exit;;
-k|--insecure) insecure="-k" ;;
--) 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 $insecure "$url")"
fi
missingHeaders="x-frame-options
strict-transport-security
content-security-policy
x-xss-protection
x-content-type-options
feature-policy
permissions-policy
cache-control
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"