|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
set -o pipefail
|
|
|
|
|
|
|
|
lotsfile="${XDG_DATA_HOME:-$HOME/.local/share}/analyse-headers/lots-domains.json"
|
|
|
|
|
|
|
|
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}"
|
|
|
|
}
|
|
|
|
|
|
|
|
fetchLots(){
|
|
|
|
# Make sure the directory exists
|
|
|
|
mkdir -p "$(dirname "$lotsfile")"
|
|
|
|
# Make sure we have jq and pup
|
|
|
|
type -p jq >/dev/null 2>&1 || die "You need jq to update lots"
|
|
|
|
# Update lots
|
|
|
|
type -p pup >/dev/null 2>&1 || die "You need pup to update lots"
|
|
|
|
# Update lots
|
|
|
|
curl https://lots-project.com/ | pup 'table#main-table tr json{}' | jq -r \
|
|
|
|
'[ .[] | . |= {
|
|
|
|
Site: .children[0]?.children[]?.text?,
|
|
|
|
Tags: [ .children[1]?.children[]?.children[]?.text ]
|
|
|
|
} | objects] | map( { (.Site): [ .Tags[] ]} ) | add' > "$lotsfile"
|
|
|
|
}
|
|
|
|
|
|
|
|
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(){
|
|
|
|
local value
|
|
|
|
value="$(echo "$1" | cut -d ':' -f 2- | tr 'A-Z' 'a-z' | trimWhitespace )"
|
|
|
|
if [ "$value" = "nosniff" ]; then
|
|
|
|
return 0
|
|
|
|
else
|
|
|
|
echo "X-Content-Type-Options" | drawInBox
|
|
|
|
wecho -e "The X-Content-Type-Options header stops a browser from \
|
|
|
|
trying to MIME-sniff the content type and forces it to stick with the \
|
|
|
|
declared content-type.\n\n"
|
|
|
|
[ -z "$1" ] && return 2
|
|
|
|
return 1
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
|
|
|
lookup_lots(){
|
|
|
|
if [ -f "$lotsfile" ]; then
|
|
|
|
jq -r '.["'"$1"'"]?[]?' "$lotsfile" | tr '\n' ',' | sed 's/,/, /g'
|
|
|
|
else
|
|
|
|
echo "No Lots File"
|
|
|
|
return 1
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
|
|
|
checkJsonp(){
|
|
|
|
# This list came from here: https://github.com/google/csp-evaluator/blob/a21f94e348b0dfb0245c65af522bf3137a9647de/allowlist_bypasses/jsonp.ts
|
|
|
|
local domains="9.chart.apis.google.com
|
|
|
|
a248.e.akamai.net
|
|
|
|
accounts.google.com
|
|
|
|
a.config.skype.com
|
|
|
|
afpeng.alimama.com
|
|
|
|
ajax.googleapis.com
|
|
|
|
an.yandex.ru
|
|
|
|
api.facebook.com
|
|
|
|
api.flickr.com
|
|
|
|
api.instagram.com
|
|
|
|
api.map.baidu.com
|
|
|
|
api.mixpanel.com
|
|
|
|
api.twitter.com
|
|
|
|
api.userlike.com
|
|
|
|
api.vk.com
|
|
|
|
appcenter.intuit.com
|
|
|
|
a.tiles.mapbox.com
|
|
|
|
autocomplete.travelpayouts.com
|
|
|
|
awaps.yandex.ru
|
|
|
|
bebezoo.1688.com
|
|
|
|
beta.gismeteo.ru
|
|
|
|
books.google.com
|
|
|
|
c1n2.hypercomments.com
|
|
|
|
c1n3.hypercomments.com
|
|
|
|
catalog.api.2gis.ru
|
|
|
|
cbks0.googleapis.com
|
|
|
|
ccrprod.alipay.com
|
|
|
|
cdn.jsdelivr.net
|
|
|
|
cdn.syndication.twimg.com
|
|
|
|
cdn.syndication.twitter.com
|
|
|
|
clients1.google.com
|
|
|
|
client.siteheart.com
|
|
|
|
community.adobe.com
|
|
|
|
connect.mail.ru
|
|
|
|
count.tbcdn.cn
|
|
|
|
cse.google.com
|
|
|
|
c.tiles.mapbox.com
|
|
|
|
d1f69o4buvlrj5.cloudfront.net
|
|
|
|
data.gongchang.com
|
|
|
|
de.blog.newrelic.com
|
|
|
|
detector.alicdn.com
|
|
|
|
dev.virtualearth.net
|
|
|
|
fast.wistia.com
|
|
|
|
fellowes.ugc.bazaarvoice.com
|
|
|
|
gdata.youtube.com
|
|
|
|
googleads.g.doubleclick.net
|
|
|
|
google.ru
|
|
|
|
googletagmanager.com
|
|
|
|
graph.facebook.com
|
|
|
|
group.aliexpress.com
|
|
|
|
gum.criteo.com
|
|
|
|
gupiao.baidu.com
|
|
|
|
h.cackle.me
|
|
|
|
ib.adnxs.com
|
|
|
|
i.cackle.me
|
|
|
|
id.rambler.ru
|
|
|
|
kecngantang.blogspot.com
|
|
|
|
links.services.disqus.com
|
|
|
|
m.addthis.com
|
|
|
|
maps.beeline.ru
|
|
|
|
maps.googleapis.com
|
|
|
|
maps.google.com
|
|
|
|
maps.google.de
|
|
|
|
maps.google.lv
|
|
|
|
maps.google.ru
|
|
|
|
mc.yandex.ru
|
|
|
|
mt1.googleapis.com
|
|
|
|
mts0.googleapis.com
|
|
|
|
mts1.googleapis.com
|
|
|
|
nominatim.openstreetmap.org
|
|
|
|
offer.alibaba.com
|
|
|
|
ok.go.mail.ru
|
|
|
|
pagead2.googlesyndication.com
|
|
|
|
partner.googleadservices.com
|
|
|
|
passport.ngs.ru
|
|
|
|
pass.yandex.com
|
|
|
|
pass.yandex.ru
|
|
|
|
pass.yandex.ua
|
|
|
|
pin.aliyun.com
|
|
|
|
pipes.yahooapis.com
|
|
|
|
plugins.mozilla.org
|
|
|
|
pro.netrox.sc
|
|
|
|
pubsub.pubnub.com
|
|
|
|
query.yahooapis.com
|
|
|
|
rec.ydf.yandex.ru
|
|
|
|
relap.io
|
|
|
|
rexchange.begun.ru
|
|
|
|
securepubads.g.doubleclick.net
|
|
|
|
se.wikipedia.org
|
|
|
|
share.yandex.net
|
|
|
|
ssl.google-analytics.com
|
|
|
|
suggest.taobao.com
|
|
|
|
syndication.twitter.com
|
|
|
|
target.ukr.net
|
|
|
|
tj.gongchang.com
|
|
|
|
translate.googleapis.com
|
|
|
|
translate.google.com
|
|
|
|
translate.yandex.net
|
|
|
|
tr.indeed.com
|
|
|
|
ulogin.ru
|
|
|
|
video.media.yql.yahoo.com
|
|
|
|
vimeo.com
|
|
|
|
wb.amap.com
|
|
|
|
widget.admitad.com
|
|
|
|
widgets.pinterest.com
|
|
|
|
wpd.b.qq.com
|
|
|
|
wslocker.ru
|
|
|
|
www.blogger.com
|
|
|
|
www.facebook.com
|
|
|
|
www.googleadservices.com
|
|
|
|
www.google-analytics.com
|
|
|
|
www.googleapis.com
|
|
|
|
www.google.com
|
|
|
|
www.google.de
|
|
|
|
www.googletagmanager.com
|
|
|
|
www.linkedin.com
|
|
|
|
www.meteoprog.ua
|
|
|
|
www-onepick-opensocial.googleusercontent.com
|
|
|
|
www.panoramio.com
|
|
|
|
www.sharethis.com
|
|
|
|
www.travelpayouts.com
|
|
|
|
www.youku.com
|
|
|
|
www.youtube.com
|
|
|
|
yandex.ru
|
|
|
|
ynuf.alipay.com"
|
|
|
|
if echo "$domains" | grep -Eq "$1"; then
|
|
|
|
return 0
|
|
|
|
else
|
|
|
|
return 1
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
|
|
|
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 we get here, the csp is set
|
|
|
|
|
|
|
|
local reportURI=false
|
|
|
|
local reportTO=false
|
|
|
|
|
|
|
|
[ -f "$lotsfile" ] || message+="WARNING: Lots file not available. Run with --fetch-lots in order to get it\n\n"
|
|
|
|
|
|
|
|
while read directive; do
|
|
|
|
local directiveName="$(echo "$directive" | cut -d ' ' -f 1)"
|
|
|
|
local directiveValue="$(echo "$directive" | cut -d ' ' -f 2-)"
|
|
|
|
case "$directiveName" in
|
|
|
|
"report-uri" ) reportURI=true ;;
|
|
|
|
"report-to" ) reportTO=true ;;
|
|
|
|
*"-src")
|
|
|
|
# check sources
|
|
|
|
while read source; do
|
|
|
|
sourcemessage=''
|
|
|
|
case "$source" in
|
|
|
|
"'self'")
|
|
|
|
sourcemessage+="This is normally fine \
|
|
|
|
although care should be taken if the site being tested has upload functionality"
|
|
|
|
ret=$((ret>1 ? ret : 1))
|
|
|
|
;;
|
|
|
|
"'unsafe-inline'")
|
|
|
|
sourcemessage+="This allows for inline assets and prevents the content \
|
|
|
|
security policy from effectively mitigating against reflected or stored XSS \
|
|
|
|
attacks"
|
|
|
|
;;
|
|
|
|
"'unsafe-eval'")
|
|
|
|
sourcemessage+="This allows for eval to be used in JS and prevents \
|
|
|
|
the content security policy from effectively mitigating against DOM based XSS \
|
|
|
|
attacks"
|
|
|
|
;;
|
|
|
|
"http:"|"https:"|"data:")
|
|
|
|
sourcemessage+="This allows the browser to source from any $source \
|
|
|
|
url. It is likely that a malicious actor could control one."
|
|
|
|
;;
|
|
|
|
"'"*)
|
|
|
|
sourcemessage+="${RED}Unknown source. URLS shouldn't have quotes around them${NC}"
|
|
|
|
;;
|
|
|
|
*)
|
|
|
|
local domain="$(echo "$source" | sed -E 's/([^/]*:\/\/)?([^/]*).*/\2/')"
|
|
|
|
lotsTags="$(lookup_lots "$domain")"
|
|
|
|
if [ $? -eq 0 ] && [ -n "$lotsTags" ]; then
|
|
|
|
sourcemessage+="The LOTS project has marked ${ORANGE}${domain}${NC} with the tags: $lotsTags."
|
|
|
|
fi
|
|
|
|
if [ "$directiveName" == "script-src" ] && checkJsonp "$domain"; then
|
|
|
|
[ -n "$sourcemessage" ] && sourcemessage+="\n\n"
|
|
|
|
sourcemessage+="${ORANGE}${domain}${NC} is known to host jsonp endpoints which can sometimes be used to bypass the CSP"
|
|
|
|
fi
|
|
|
|
sourcemessage="$(echo -n "$sourcemessage")"
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
|
|
|
|
if [ -n "$sourcemessage" ]; then
|
|
|
|
message+="The directive \
|
|
|
|
${ORANGE}$directiveName${NC} has a value of ${ORANGE}${source}${NC}.\n${sourcemessage}\n\n"
|
|
|
|
fi
|
|
|
|
|
|
|
|
done < <(echo "$directiveValue" | tr ' ' '\n' | trimWhitespace)
|
|
|
|
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
|
|
|
|
done < <(echo "$value" | tr ';' '\n' | trimWhitespace)
|
|
|
|
|
|
|
|
if [ "$reportTO" == "false" ]; then
|
|
|
|
message+="The content security policy doesn't include the \
|
|
|
|
${ORANGE}report-to${NC} directive which is used to report CSP violations.\n\n"
|
|
|
|
ret=$((ret>1 ? ret : 1))
|
|
|
|
|
|
|
|
fi
|
|
|
|
if [ "$reportURI" == "false" ]; then
|
|
|
|
message+="The content security policy doesn't include the \
|
|
|
|
${ORANGE}report-uri${NC} directive which is used to report CSP violations. \
|
|
|
|
Eventually the report-to header will deprecate this directive, but it is not \
|
|
|
|
yet supported in most browsers so including both is recomended.\n\n"
|
|
|
|
ret=$((ret>1 ? ret : 1))
|
|
|
|
fi
|
|
|
|
|
|
|
|
# elif echo "$value" | grep -q 'unsafe-eval'; then
|
|
|
|
# ret=$((ret>1 ? ret : 1))
|
|
|
|
# fi
|
|
|
|
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 -qi '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 -qi "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 -qi "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
|
|
|
|
--fetch-lots Updates domains from https://lots-project.com/ 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
|
|
|
|
|
|
|
|
insecure=""
|
|
|
|
|
|
|
|
# Read the options and set stuff
|
|
|
|
while [[ $1 = -?* ]]; do
|
|
|
|
case $1 in
|
|
|
|
-h|--help) usage; exit;;
|
|
|
|
-k|--insecure) insecure="-k" ;;
|
|
|
|
--fetch-lots ) fetchLots; 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 $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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|