HackBlue

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.

9 min read · practice only with authorization

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.

Snippet
#!/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.

Snippet
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?