Writing a Small Audit Script: Checking HTTP Security Headers
Time to write a complete tool, small but useful: a script that fetches one web page from a site you own and tells you which standard HTTP security headers are present or missing. It is a safe, defensive audit that a real developer would be glad to receive, and a perfect portfolio piece.
Only your own site, and only in scope
Even a simple request tool must be pointed only at a system you own or are authorized to test, such as OWASP Juice Shop or DVWA running on your own computer, a PortSwigger lab, or a TryHackMe or HackTheBox machine. Sending requests to other people's sites without permission can fall under articles 323-1 to 323-3-1 of the Code pénal in France. The script we write will refuse any host that is not on an allowed list.
What are security headers?
When a server sends a web page, it also sends headers, short instructions for the browser. Some of them make the browser enforce safer behavior. The documentation maintained by Mozilla (developer.mozilla.org) and the OWASP project (owasp.org) describe them in detail. These are the ones our script will check:
- Content-Security-Policy: tells the browser where scripts and other resources may come from. A strong policy greatly reduces the impact of injected scripts.
- Strict-Transport-Security: tells the browser to use HTTPS only for this site in the future. It is meaningful only when the site is served over HTTPS.
- X-Content-Type-Options: with the value
nosniff, stops the browser from guessing a file's type. - X-Frame-Options (or the
frame-ancestorspart of the policy): controls whether the page can be embedded in a frame on another site, which protects against clickjacking. - Referrer-Policy: controls how much of the previous address is shared with other sites.
- Permissions-Policy: restricts powerful browser features, such as camera or location, that the page may use.
A missing header is not automatically a vulnerability, and the presence of a header does not mean a site is secure. It is a hygiene indicator, and a reason to ask a developer a question.
Design before code
Good scripts start with a plan. Ours will:
- Take one URL on the command line.
- Refuse to run unless the host is on an allowed list.
- Send a single GET request with a timeout and an honest
User-Agent. - Compare the response headers to a checklist.
- Print a clear report and return an exit code that other tools can use.
One request, no crawling, no guessing, no fuzzing. That is a deliberate limit.
The full script
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
ALLOWED_HOSTS = {"localhost", "127.0.0.1"}
CHECKS = {
"Content-Security-Policy": "limits where scripts and resources can load from",
"Strict-Transport-Security": "forces HTTPS on later visits (HTTPS sites only)",
"X-Content-Type-Options": "should be nosniff to stop type guessing",
"X-Frame-Options": "protects against embedding in other sites",
"Referrer-Policy": "limits what is shared with other sites",
"Permissions-Policy": "restricts powerful browser features",
}
def fetch_headers(url):
request = Request(url, headers={"User-Agent": "lab-header-audit/1.0"})
try:
with urlopen(request, timeout=5) as response:
return response.headers
except HTTPError as error:
return error.headers # 404 or 500 pages still have headers
def main():
if len(sys.argv) != 2:
print("usage: python3 header_audit.py http://localhost:3000")
return 2
url = sys.argv[1]
host = urlparse(url).hostname
if host not in ALLOWED_HOSTS:
print(f"Refused: {host} is not in ALLOWED_HOSTS. Only audit systems you own.")
return 2
try:
headers = fetch_headers(url)
except (URLError, TimeoutError) as error:
print(f"Could not reach {url}: {error}")
return 1
missing = 0
for name, purpose in CHECKS.items():
value = headers.get(name)
if value:
print(f"[present] {name}: {value}")
else:
missing += 1
print(f"[missing] {name} - {purpose}")
print(f"{missing} of {len(CHECKS)} checked headers are missing.")
return 0 if missing == 0 else 3
if __name__ == "__main__":
sys.exit(main())
Walking through the important parts
ALLOWED_HOSTSis the scope guard from lesson 2.2 applied to web targets. The script refuses anything else before sending a single packet.timeout=5prevents the script from hanging forever.- The custom
User-Agentis honest about what the tool is, which helps whoever reads the server logs. headers.get(name)is case-insensitive for this kind of object, which matters because servers write header names in different cases.- Exit codes: 0 means "all present", 3 means "some missing", 1 means "unreachable" and 2 means "bad usage or refused". Other scripts, or a development pipeline, can use them.
- Catching
HTTPError: a 404 or 500 response raises an exception in this library, but its headers are still useful.
Running it in your lab
Start OWASP Juice Shop on your own machine following its official instructions. It listens on a local port, commonly 3000, so the address is http://localhost:3000. Then:
python3 header_audit.py http://localhost:3000
Read the report. Some headers will probably be missing: that is expected from a deliberately vulnerable training application. Notice that Strict-Transport-Security will be reported even though the lab uses plain HTTP; this is a good moment to remember the note above that the header only matters on HTTPS, and to mention it in your README.
Improving it step by step
A first version that works beats a perfect version that does not exist. Here is how to grow it, one improvement at a time:
- Skip the Strict-Transport-Security check when the URL starts with
http://. - Warn when headers leak server details, for example a
ServerorX-Powered-Byheader that shows exact software versions. - Check the cookie attributes
Secure,HttpOnlyandSameSiteinSet-Cookieheaders. - Output a JSON report so another tool can use it.
- Read the allowed hosts from a small configuration file instead of the code.
Test each change on its own and keep the old version in your repository history so that you can go back.
What this script is not
It is not a vulnerability scanner, and it does not prove a site is secure or insecure. It does not exploit anything. In a real report, a missing header is typically a low-severity hygiene observation, and you should say so honestly rather than inflate it. Credibility is your main asset as a tester.
Your turn: add the HTTPS rule and a leak check
Modify the script so that (1) the Strict-Transport-Security check is skipped with a note when the URL starts with http://, and (2) a warning is printed if a Server or X-Powered-By header exists.
See a solution
LEAKY = ("Server", "X-Powered-By")
def main():
# ... same start as before ...
is_https = url.lower().startswith("https://")
missing = 0
for name, purpose in CHECKS.items():
if name == "Strict-Transport-Security" and not is_https:
print(f"[skipped] {name} - only meaningful over HTTPS")
continue
value = headers.get(name)
if value:
print(f"[present] {name}: {value}")
else:
missing += 1
print(f"[missing] {name} - {purpose}")
for name in LEAKY:
value = headers.get(name)
if value:
print(f"[warning] {name} reveals: {value}")
print(f"{missing} checked headers are missing.")
return 0 if missing == 0 else 3
The summary no longer divides by the total number of checks, because one check may have been skipped. Keeping the count of skipped checks separate would be an even better refinement.
Portfolio step
Publish header_audit.py in 02-python-security with a README containing: purpose, usage, the safety guard-rail, an example output from your own Juice Shop lab and a clear "authorized use only" statement. This is your first complete, defensive tool, and a good conversation starter for interviews.
Key takeaways
- Security headers are instructions for the browser; their absence is a hygiene signal, not automatically a vulnerability.
- A responsible tool enforces an allowed-host list, uses timeouts, sends one honest request and returns clear exit codes.
- Plan first, build a small version, test it, then improve it one feature at a time.
- Report findings honestly and do not inflate low-severity observations.
- Run it only against systems you own or are authorized to test, such as your own Juice Shop or DVWA.
You now have four small scripts in your portfolio. In chapter 3 you will step into the world of teams: the attackers who simulate and the defenders who detect.