HTTP security headers
medium external
What it inspects
Looks for this finding: HTTPS responses are missing or weakly set core security headers. Sends one HTTPS HEAD request for / to port 443 (certificate not verified, scanned name used as SNI and Host) and reads the headers of that first response; redirects are not followed. WARN if any of Strict-Transport-Security, Content-Security-Policy, X-Frame-Options or X-Content-Type-Options is missing or has a weak value: HSTS max-age of 0 or a repeated max-age (a max-age under 6 months only adds a note), a CSP whose script sources allow any origin, 'unsafe-eval' or 'unsafe-inline' without a nonce or hash, a CSP with a frame-ancestors that lets any site frame the page (every policy that sets it allows * or a bare scheme), a CSP that sets script-src-elem or script-src-attr without script-src or default-src (unless it also has a restrictive frame-ancestors), a CSP that sets none of script-src, default-src or frame-ancestors, X-Frame-Options other than DENY or SAMEORIGIN, X-Content-Type-Options other than nosniff. A CSP that only sets a restrictive frame-ancestors is accepted. A wildcard is judged as a whole TLD only for a single label (https://*.com); multi-label public suffixes such as *.co.uk are not recognised. Content-Security-Policy-Report-Only does not count. If nothing answers HTTPS on 443 the check is skipped. This is a passive check: it sends only what a normal client would.
Why it matters
Finding: HTTPS responses are missing or weakly set core security headers. These headers switch on protections the browser already has: HSTS stops downgrade to plain HTTP, X-Frame-Options and CSP frame-ancestors stop other sites framing yours for clickjacking, X-Content-Type-Options stops MIME sniffing, and a full CSP limits where scripts may come from. Missing headers breach nothing by themselves, but they remove cheap layers of defence.
How to verify
# only against hosts you own
curl -sI https://example.com | grep -iE '^(strict-transport-security|content-security-policy|x-frame-options|x-content-type-options):'
# All four header names should be printed, with these values: Strict-Transport-Security has max-age greater than 0 (once stable, raise it to
# 15768000 or more; a repeated max-age or max-age=0 is a WARN), Content-Security-Policy has a script-src (or default-src, when script-src is
# absent) that does not allow *, a bare scheme such as https:, or 'unsafe-eval', and allows 'unsafe-inline' only next to a nonce or hash;
# a frame-ancestors of 'self' or 'none' alone is also accepted, but frame-ancestors * is a WARN,
# X-Frame-Options is DENY or SAMEORIGIN, X-Content-Type-Options is nosniff. (curl -I sends a HEAD request, like the check.)
How to fix
Clears the finding "HTTPS responses are missing or weakly set core security headers". Add the four headers at http level in nginx or in Apache's configuration with a validated file that rolls itself back, or by hand in Caddy, then tighten HSTS and CSP deliberately.
Add the four headers in nginx and Apache
Any distro
# These headers apply to EVERY site on this server (nginx http level, Apache server-wide). If any site must be framed by another origin
# (widgets, payment or booking iframes, LMS or SSO embeds), skip this step and set the headers per site as described in the next step.
# nginx: sets the four headers for every server in this nginx (http level). Assumes the stock layout where nginx.conf includes
# /etc/nginx/conf.d/*.conf (check: grep -n 'conf.d' /etc/nginx/nginx.conf). Skipped when nginx is not installed.
# A server or location that has its own add_header lines does NOT inherit these: see the next step.
# Where your application already sends one of these headers, the response can carry it twice; remove one source.
if command -v nginx >/dev/null 2>&1 && [ -d /etc/nginx/conf.d ]; then
install -m 0644 /dev/stdin /etc/nginx/conf.d/umbermark-security-headers.conf <<'EOF'
# HSTS is deliberately short (max-age=300, five minutes) so a mistake fixes itself. Raising it, includeSubDomains and preload
# are hard to undo: see the last step before you add any of them.
add_header Strict-Transport-Security "max-age=300" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
EOF
nginx -t && systemctl try-reload-or-restart nginx
if ! nginx -t >/dev/null 2>&1; then rm -f /etc/nginx/conf.d/umbermark-security-headers.conf; echo 'nginx -t failed: file removed, nothing reloaded'; fi
fi
# Apache on Debian/Ubuntu (apache2): applies to every site. setifempty skips a header Apache already has, but a proxied or CGI backend's own header can still be sent as well (duplicates are possible).
if [ -d /etc/apache2/conf-available ] && command -v a2enconf >/dev/null 2>&1; then
install -m 0644 /dev/stdin /etc/apache2/conf-available/umbermark-security-headers.conf <<'EOF'
# HSTS is deliberately short (max-age=300, five minutes) so a mistake fixes itself. Raising it, includeSubDomains and preload
# are hard to undo: see the last step before you add any of them.
<IfModule mod_headers.c>
Header always setifempty Strict-Transport-Security "max-age=300"
Header always setifempty Content-Security-Policy "frame-ancestors 'self'"
Header always setifempty X-Frame-Options "SAMEORIGIN"
Header always setifempty X-Content-Type-Options "nosniff"
</IfModule>
EOF
a2enmod headers
a2enconf umbermark-security-headers
apachectl configtest && systemctl try-reload-or-restart apache2
if ! apachectl configtest >/dev/null 2>&1; then a2disconf umbermark-security-headers; rm -f /etc/apache2/conf-available/umbermark-security-headers.conf; echo 'apachectl configtest failed: configuration removed, nothing reloaded'; fi
fi
# Apache on the RHEL family (httpd): mod_headers is loaded by default there.
if [ -d /etc/httpd/conf.d ]; then
install -m 0644 /dev/stdin /etc/httpd/conf.d/umbermark-security-headers.conf <<'EOF'
# HSTS is deliberately short (max-age=300, five minutes) so a mistake fixes itself. Raising it, includeSubDomains and preload
# are hard to undo: see the last step before you add any of them.
<IfModule mod_headers.c>
Header always setifempty Strict-Transport-Security "max-age=300"
Header always setifempty Content-Security-Policy "frame-ancestors 'self'"
Header always setifempty X-Frame-Options "SAMEORIGIN"
Header always setifempty X-Content-Type-Options "nosniff"
</IfModule>
EOF
apachectl configtest && systemctl try-reload-or-restart httpd
if ! apachectl configtest >/dev/null 2>&1; then rm -f /etc/httpd/conf.d/umbermark-security-headers.conf; echo 'apachectl configtest failed: file removed, nothing reloaded'; fi
fi
- Add them by hand where the step above does not reach (nginx servers with their own headers, Caddy, CDN)
- Strengthen HSTS and CSP later, carefully
2 more steps in the Fix Pack. See pricing