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.
848 lines
24 KiB
848 lines
24 KiB
#!/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" |
|
|
|
|
|
|
|
|
|
|