Tools · Python and Bash for security automation · 03 / 20
Checking your website’s security headers with Python requests
Write a script that fetches your own sites and reports missing or weak security headers such as HSTS, Content-Security-Policy, and cookie flags.
Headers to check
Strict-Transport-Security forces HTTPS on later visits. Content-Security-Policy restricts where scripts and other resources can load from. X-Content-Type-Options: nosniff stops MIME sniffing. Referrer-Policy limits referrer leakage. Permissions-Policy restricts browser features. A frame-ancestors directive in CSP (or X-Frame-Options) prevents clickjacking. Cookies should have Secure, HttpOnly where possible, and a SameSite value.
The script
requests follows redirects by default, so check the final URL and also record whether HTTP redirects to HTTPS.
#!/usr/bin/env python3
"""Report security headers for URLs you own."""
import sys, requests
WANT = {
'strict-transport-security': lambda v: 'max-age=' in v and int(v.split('max-age=')[1].split(';')[0]) >= 15552000,
'content-security-policy': lambda v: "default-src" in v or "script-src" in v,
'x-content-type-options': lambda v: v.lower() == 'nosniff',
'referrer-policy': lambda v: bool(v),
}
def check(url):
r = requests.get(url, timeout=10)
print(f'\n{url} -> {r.url} [{r.status_code}]')
for name, ok in WANT.items():
v = r.headers.get(name)
print(f' {name:28} {"MISSING" if v is None else ("ok" if ok(v) else "weak")} {v or ""}'[:140])
csp = r.headers.get('content-security-policy', '')
if 'frame-ancestors' not in csp and 'x-frame-options' not in r.headers:
print(' framing protection MISSING')
for c in r.cookies:
flags = [f for f, on in [('Secure', c.secure), ('HttpOnly', c.has_nonstandard_attr('HttpOnly')), ('SameSite', c.has_nonstandard_attr('SameSite'))] if not on]
if flags:
print(f' cookie {c.name}: missing {", ".join(flags)}')
plain = requests.get(url.replace('https://', 'http://', 1), timeout=10, allow_redirects=False)
print(' http->https redirect ', 'ok' if plain.is_redirect and plain.headers.get('location', '').startswith('https://') else 'MISSING')
for u in sys.argv[1:]:
check(u)Lab: before and after
Run the script against a lab nginx site with default settings and note the missing headers. Add the headers below to the server block, reload, and run again. Then open the site in a browser and check the console for CSP violations, which tell you which sources your page really needs.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'" always; add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy strict-origin-when-cross-origin always; python3 headers.py https://lab.internal
Limits
Headers are defense in depth. A strict CSP does not fix a cross-site scripting bug, but it can stop it from loading attacker scripts. Use the script for your own properties only, and pair it with tools like Mozilla Observatory for a broader review.
Diagram
Security headers and what they do
Defense in depth for browsers.
- Strict-Transport-Security
Always use HTTPS for this site.
- Content-Security-Policy
Limit script and resource sources; frame-ancestors stops framing.
- X-Content-Type-Options
nosniff: trust the declared content type.
- Referrer-Policy
Limit what URLs leak to other sites.
- Cookie flags
Secure, HttpOnly, SameSite.
Check yourself
1. Which header tells browsers to use HTTPS for future visits?
2. Which CSP directive prevents other sites from framing your pages?
3. Which cookie flag prevents JavaScript from reading a cookie?