Fetch a target URL and audit its HTTP response headers for missing or misconfigured security headers.
Warning
Send requests only to sites you own or are authorized to test. This tool makes a single benign GET/HEAD request per URL — no fuzzing, no auth bypass — but any request to a third party is still traffic against their infrastructure. Use a local app or your own site for the lab.
Only run these labs against systems you own or are explicitly authorized to test.
- Issue an HTTP request and read response headers with
requests. - Check for the presence of key security headers.
- Detect information-leaking headers (
Server,X-Powered-By). - Produce a pass/fail audit report with remediation hints.
After completing this lab you will be able to:
- Issue HTTP requests and inspect response headers programmatically.
- Explain what each major security header does and what its absence permits.
- Grade a site's header posture and produce actionable remediation advice.
- Handle redirects, timeouts, and TLS errors without crashing.
- Describe why headers are one signal among many, not a security verdict.
- [[requests]] — HTTP sessions and response objects (or [[Urllib-Module|urllib]] for a stdlib-only version).
- [[Dictionaries]] — headers are a case-insensitive mapping.
- [[Error-and-Exception-Handling/Readme|Error & Exception Handling]] — network failures are routine.
- [[SSL-Module|SSL Module]] — understanding TLS errors you will encounter.
| Item | Detail |
|---|---|
| Python | 3.8+ |
| Package | requests (pip install requests) |
| Target | A local test app (started in Setup) or your own site |
mkdir -p ~/labs/header-audit && cd ~/labs/header-audit
python3 -m venv .venv
source .venv/bin/activate
pip install requests
# Start a local app that deliberately omits security headers:
python3 -m http.server 8080 & # serves the current dir on :8080
nano audit.py # paste the Code section- Fetch a response. Request a URL with a timeout and print the status code and final URL after redirects.
- List the headers. Print every response header; note that the mapping is case-insensitive.
- Define the checklist. Build a dictionary of the headers you care about —
Strict-Transport-Security,Content-Security-Policy,X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy. - Check presence. Report each as present or missing, with a one-line explanation of the risk when absent.
- Check values, not just presence. A
Content-Security-Policycontainingunsafe-inlineis weaker than one without; HSTS with a very shortmax-ageprovides little protection. - Flag information disclosure. Report
ServerandX-Powered-Byvalues that leak product versions. - Grade and report. Produce a simple score with concrete remediation for each gap.
#!/usr/bin/env python3
"""Audit a URL's HTTP response for security-header hygiene."""
import argparse
import sys
import requests
# header -> why it matters (shown when missing)
EXPECTED = {
"Strict-Transport-Security": "Forces HTTPS; prevents SSL-strip downgrade.",
"Content-Security-Policy": "Mitigates XSS and data-injection.",
"X-Content-Type-Options": "Stops MIME-sniffing (set to 'nosniff').",
"X-Frame-Options": "Prevents clickjacking via framing.",
"Referrer-Policy": "Controls Referer leakage to third parties.",
"Permissions-Policy": "Restricts powerful browser features.",
}
# Headers that leak stack/version info if present.
LEAKY = ["Server", "X-Powered-By", "X-AspNet-Version", "X-Generator"]
def audit(url: str, timeout: float = 10.0) -> int:
try:
resp = requests.get(url, timeout=timeout, allow_redirects=True)
except requests.RequestException as exc:
print(f"[!] Request failed: {exc}")
return 2
print(f"[*] {resp.status_code} {resp.reason} {resp.url}\n")
headers = {k.lower(): v for k, v in resp.headers.items()}
print("Security headers:")
missing = 0
for name, reason in EXPECTED.items():
if name.lower() in headers:
print(f" [PASS] {name}: {headers[name.lower()]}")
else:
missing += 1
print(f" [FAIL] {name} — MISSING ({reason})")
print("\nInformation disclosure:")
leaks = 0
for name in LEAKY:
if name.lower() in headers:
leaks += 1
print(f" [WARN] {name}: {headers[name.lower()]} (consider removing)")
if not leaks:
print(" [OK] No obvious version-leaking headers.")
print(f"\n[*] {missing} missing security header(s), {leaks} leaky header(s).")
return 1 if (missing or leaks) else 0
def main() -> None:
parser = argparse.ArgumentParser(description="HTTP security-header auditor.")
parser.add_argument("url", help="Full URL, e.g. https://example.com")
args = parser.parse_args()
sys.exit(audit(args.url))
if __name__ == "__main__":
main()$ python audit.py http://127.0.0.1:8080
[*] 200 OK http://127.0.0.1:8080/
Security headers:
[FAIL] Strict-Transport-Security — MISSING (Forces HTTPS; prevents SSL-strip downgrade.)
[FAIL] Content-Security-Policy — MISSING (Mitigates XSS and data-injection.)
[FAIL] X-Content-Type-Options — MISSING (Stops MIME-sniffing (set to 'nosniff').)
[FAIL] X-Frame-Options — MISSING (Prevents clickjacking via framing.)
[FAIL] Referrer-Policy — MISSING (Controls Referer leakage to third parties.)
[FAIL] Permissions-Policy — MISSING (Restricts powerful browser features.)
Information disclosure:
[WARN] Server: SimpleHTTP/0.6 Python/3.11.2 (consider removing)
[*] 6 missing security header(s), 1 leaky header(s).
requests.getwithallow_redirects=Truefollows the redirect chain and reports the final URL's headers — important because HSTS and CSP are often only set on the HTTPS endpoint after an HTTP→HTTPS redirect.- Case-insensitive lookup (
k.lower()) matters because HTTP header names are case-insensitive; a server may sendcontent-security-policyorContent-Security-Policy. - Missing security headers are common real findings: no CSP means the site relies entirely on other XSS defences; no HSTS leaves the first request downgradeable.
- Leaky headers like
Server: Apache/2.4.29hand an attacker a version to match against a CVE list. Suppressing them is defence-in-depth, not a fix. - The script returns a non-zero exit code on any finding, so it can gate a CI pipeline (
python audit.py $URL || echo "harden headers").
python3 headeraudit.py http://127.0.0.1:8000[*] http://127.0.0.1:8000 -> 200 OK
MISSING Strict-Transport-Security HTTPS not enforced on future visits
MISSING Content-Security-Policy No restriction on script sources (XSS risk)
MISSING X-Content-Type-Options MIME-type sniffing permitted
MISSING X-Frame-Options Page can be framed (clickjacking risk)
INFO Server: SimpleHTTP/0.6 Python/3.13.7 version disclosure
Score: 0/6 security headers present
- A bare
http.serverinstance scores 0/6 — it sets no security headers. - Adding a header via a proxy or a custom handler changes the score.
- An unreachable host produces a clear error, not a traceback.
- A redirect is followed and the final URL is reported.
- Header lookup is case-insensitive (
content-security-policymatches).
- Read a list of URLs from a file and audit each, printing a summary table.
- Score each site A–F based on how many headers pass (mirror securityheaders.com).
- Parse the CSP value and warn on weakeners like
unsafe-inlineor*. - Add a
--jsonflag emitting machine-readable results for a dashboard. - Check cookies for the
Secure,HttpOnly, andSameSiteattributes and report insecure ones.
| Symptom | Likely cause | Fix |
|---|---|---|
ConnectionError |
Nothing listening on the target port | Start the local server from the Setup step |
SSLError: certificate verify failed |
Self-signed certificate in the lab | Fix the trust store, or use verify= with the CA bundle — do not reach for verify=False |
| Script hangs | No timeout on the request | Pass timeout=10 to every request |
| Header reported missing but visible in the browser | Header set on a redirect target, not the first response | Inspect response.history as well as the final response |
KeyError on a header |
Direct indexing instead of .get() |
Use response.headers.get(name) |
ModuleNotFoundError: requests |
Package not installed in the active venv | python3 -m pip install requests, or use the urllib variant |
- Audit only sites you own or are authorized to test. Requesting a page is low impact, but automated scanning across a domain is still unauthorized activity without permission.
- Never disable TLS verification.
verify=Falsemakes the audit meaningless and trains a bad habit — a tool that ignores certificate errors cannot report on them. - Headers are one signal, not a verdict. A perfect header score says nothing about authentication, authorization, or injection flaws. Do not let a green report imply the application is secure.
ServerandX-Powered-Bydisclosure is low severity on its own but is useful to an attacker for version-specific exploit selection. Report it as informational.- Respect rate limits and
robots.txtif you extend this to crawl multiple pages. - Set a descriptive
User-Agentidentifying your tool and the engagement, so the target's operators can attribute the traffic.
kill %1 2>/dev/null # stop the local http.server
deactivate
rm -rf ~/labs/header-audit- MDN — HTTP headers
- OWASP — Secure Headers Project
- MDN — Content Security Policy
- MDN — Strict-Transport-Security
- requests documentation
- [[API-Client]] — related Mini-Project
- [[Subdomain-Enumerator]] — feed live hosts into this auditor
- [[Readme|Python for Security Professionals]] — course home