ScreenshotNeo

BlogHow-to

Secure Headers Test: Check HTTP Security Response Headers

Learn how to check HTTP security response headers with curl, Python, Node.js, browser tools, and a practical interpretation workflow.

By the ScreenshotNeo team29 September 20269 min read

Secure Headers Test: Check HTTP Security Response Headers

A secure headers test checks the HTTP responses that a website actually sends and evaluates whether important browser security policies are present and appropriate. You can perform the check with curl, Python, Node.js, browser developer tools, or an HTTP header scanner.

The direct workflow is:

  1. Choose the exact HTTPS URL and any important application paths.
  2. Record the redirect chain, final status, and response headers.
  3. Review Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy.
  4. Compare each policy with the resources and browser features your application really uses.
  5. Retest representative pages, redirects, APIs, error responses, and authenticated routes.

A scan is a configuration signal, not proof that a site is secure or a complete security audit. MDN’s Observatory documentation warns that API results may not accurately represent an API’s overall security posture.

What HTTP security headers do

Content-Security-Policy (CSP)

Content-Security-Policy controls which resources a browser may load for a document. Directives can restrict scripts, styles, images, fonts, connections, frames, and other resource categories. A policy can reduce the impact of cross-site scripting, but it must match the application’s actual dependencies. A copied preset may block legitimate analytics, payment widgets, fonts, or API calls.

Deliver CSP in the response header; MDN states, “A CSP should be delivered to the browser in the Content-Security-Policy response header.” Before enforcing a new policy, use Content-Security-Policy-Report-Only to observe violations without blocking resources. Review reports, remove unnecessary sources, and then enforce the smallest policy that supports the application. The upgrade-insecure-requests directive can help migrate URLs inside a page, but it does not replace HSTS.

Primary references: MDN CSP overview and MDN CSP guide.

Strict-Transport-Security (HSTS)

HSTS tells a browser to use HTTPS for future connections to a hostname. Browsers ignore HSTS received over insecure HTTP, so send it from an HTTPS response. The policy applies to a hostname rather than an IP address. includeSubDomains extends the rule to subdomains and therefore requires those subdomains to support HTTPS as well.

HSTS normally cannot protect the first visit, because the browser has not learned the policy yet. Preloading can reduce that first-connection gap but has broad, domain-wide consequences. Treat includeSubDomains and preload as deployment decisions, not automatic scanner fixes. See MDN’s Strict-Transport-Security reference.

X-Content-Type-Options

The useful value is nosniff. It tells browsers to respect the MIME type in Content-Type instead of trying to infer another type. For scripts and styles, browsers can block a response when the declared type does not match the expected JavaScript or CSS type. This header does not repair an incorrectly typed response: serve JavaScript, CSS, JSON, images, and downloads with correct MIME types. See MDN’s X-Content-Type-Options reference.

Referrer-Policy

Referrer-Policy controls how much URL information accompanies outbound requests. no-referrer sends none. same-origin limits the referrer to same-origin requests. strict-origin-when-cross-origin sends the full URL for same-origin requests, only the origin for qualifying cross-origin HTTPS requests, and nothing when moving from HTTPS to a less secure destination. MDN identifies strict-origin-when-cross-origin as the default when no valid policy is supplied. Choose deliberately when URLs contain identifiers or sensitive path data. See MDN’s Referrer-Policy reference.

Permissions-Policy

Permissions-Policy controls access to selected browser features in the document and its frames. The correct allowlist depends on the features your application and embedded content need. The MDN reference labels this feature experimental, so check current browser behavior before applying a generic denylist or allowlist. See MDN’s Permissions Policy documentation.

Run a secure headers test with cURL

Start with the final response and include headers:

A header test follows the response from request to browser and evaluates the policies the browser receives.
A header test follows the response from request to browser and evaluates the policies the browser receives.
curl -sS -D - -o /dev/null https://example.com/

-D - prints response headers and -o /dev/null discards the body. To inspect redirects, use:

curl -sS -L -D /tmp/headers.txt -o /dev/null https://example.com/

With -L, cURL follows redirects. Review each response block in the output; a redirect can have different headers from the final page. To show status and URL separately:

curl -sS -L -o /dev/null -w 'final=%{url_effective}\nstatus=%{http_code}\n' https://example.com/

Test HTTP explicitly to verify the upgrade path:

curl -sS -D - -o /dev/null http://example.com/

For an API or a route that varies by method, add the method and representative headers:

curl -sS -X GET -H 'Accept: application/json' -D - -o /dev/null https://example.com/api/profile

Do not send credentials to a third-party scanner or paste private response headers into a public issue. Use a controlled environment for authenticated endpoints.

Check headers with Python

Install Requests with python -m pip install requests, then save this as secure_headers.py:

import sys
from urllib.parse import urljoin
import requests

url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com/"
r = requests.get(url, allow_redirects=True, timeout=30)

print("Requested:", url)
print("Final URL:", r.url)
print("Status:", r.status_code)
print("Redirects:", len(r.history))
for i, hop in enumerate(r.history, 1):
    print(f"Redirect {i}: {hop.status_code} {hop.url} -> {hop.headers.get('Location')}")

names = [
    "content-security-policy",
    "strict-transport-security",
    "x-content-type-options",
    "referrer-policy",
    "permissions-policy",
]
for name in names:
    value = r.headers.get(name)
    print(f"{name}: {value if value is not None else '[missing]'}")

print("Content-Type:", r.headers.get("content-type", "[missing]"))

Run it with python secure_headers.py https://example.com/. Requests follows redirects by default in this example, while r.history preserves the intermediate responses. For a page requiring a session, create a requests.Session(), set only the required cookies, and keep credentials out of logs.

Check headers with Node.js

Node.js 18 or newer includes fetch. Save this as secure-headers.mjs:

const target = process.argv[2] || 'https://example.com/';
const response = await fetch(target, { redirect: 'manual' });

console.log('Requested:', target);
console.log('Status:', response.status);
console.log('Location:', response.headers.get('location') || '[none]');

for (const name of [
  'content-security-policy',
  'strict-transport-security',
  'x-content-type-options',
  'referrer-policy',
  'permissions-policy',
  'content-type'
]) {
  console.log(`${name}: ${response.headers.get(name) || '[missing]'}`);
}

Run node secure-headers.mjs https://example.com/. The example uses redirect: 'manual' so you can inspect every hop. Follow redirects yourself when you need the complete chain:

let url = target;
for (let hop = 0; hop < 10; hop++) {
  const r = await fetch(url, { redirect: 'manual' });
  console.log(r.status, url, r.headers.get('location') || '');
  const location = r.headers.get('location');
  if (!location || r.status < 300 || r.status >= 400) break;
  url = new URL(location, url).href;
}

Use browser developer tools

  1. Open the page in a browser and open Developer Tools.
  2. Select the Network panel and reload with the network log preserved.
  3. Click the document request, then inspect Response Headers.
  4. Inspect redirect responses and representative subresources, not only the first document.
  5. Use the Security panel to confirm the connection and certificate details separately from header policy.

Browser tools show what that browser received. They do not prove that every CDN edge, locale, authentication state, API route, or error page returns the same headers.

How to interpret a scanner result

Before acting on a finding, record the tested hostname, exact URL, status code, redirect chain, timestamp, and scanner scope. Then classify the result:

Finding Questions to ask
Missing header Is this response path in scope? Does a CDN or proxy remove it? Is the policy needed for this application?
Present but unsuitable Does the value match the resources, embeds, redirects, and browser features actually used?
Different across routes Which server, framework middleware, cache rule, or origin generated each response?
Scanner grade or score Which rules and paths were checked, and what was excluded?

For CSP, inventory scripts, styles, images, fonts, connections, frames, workers, and reporting endpoints. Deploy a report-only policy while observing violations. For HSTS, confirm the header is on HTTPS responses and understand the effect of includeSubDomains or preload. For nosniff, verify every script and stylesheet has a correct MIME type. For referrer policy, examine whether URLs expose sensitive values. For Permissions-Policy, verify feature support and embedded-frame requirements.

Test more than the homepage

  • HTTP and HTTPS versions, including the redirect response.
  • Canonical pages, login pages, logout pages, and authenticated application routes.
  • API responses, JSON errors, file downloads, images, JavaScript, and CSS.
  • 404, 403, 429, and 500 responses.
  • CDN and origin responses from relevant regions if your infrastructure varies by edge.
  • Different hostnames, including subdomains affected by HSTS.

Header policies can be added by an application server, reverse proxy, CDN, hosting platform, or web server. Identify the layer that owns the response before editing configuration.

Common errors and fixes

“The header is present in the browser but missing in cURL”

Check whether you requested a different URL, user agent, method, or redirect hop. Compare the exact request and inspect proxy rules that vary by client.

“HSTS does not appear on HTTP”

This is expected: browsers ignore HSTS delivered over insecure HTTP. Verify it on the HTTPS response and test the HTTP-to-HTTPS redirect separately.

“Adding CSP broke the page”

Switch the proposed policy to Content-Security-Policy-Report-Only, collect violations, identify legitimate dependencies, and tighten the policy incrementally. Avoid adding broad wildcards without understanding the trade-off.

“nosniff blocks my script”

Inspect the script response’s Content-Type. Serve JavaScript with a JavaScript MIME type and CSS with a CSS MIME type; do not use nosniff as a substitute for correct typing.

“The scanner reports different results on different runs”

Compare the URL, redirect chain, cache state, authentication, edge location, and response status. Dynamic deployments and CDN configuration can produce different headers.

“Permissions-Policy recommendation is unclear”

List the browser features used by the page and its frames, check current browser support, and define only the permissions your application needs. A generic policy is not universally correct.

Performance, reliability, and cost considerations

Header checks are lightweight because they can discard the response body. Use timeouts, cap redirect depth, and avoid scanning the same immutable response repeatedly. For production monitoring, store normalized header values and response metadata so changes are diffable. Alert on unexpected removals or policy changes rather than on every score variation.

A clean capture removes common overlays before recording the page.
A clean capture removes common overlays before recording the page.

Run checks from more than one vantage point when a CDN, geo-routing, or WAF may change responses. Test after deployments and proxy changes. Keep scanner credentials and authenticated cookies out of telemetry. A header scan has no inherent cost beyond the HTTP requests and the service you choose; commercial scanners may apply their own limits or retention terms, so review those terms before submitting sensitive hostnames.

Or skip the browser setup

If you also need a visual record of the page after checking its headers, ScreenshotNeo can capture it with one GET request. The API returns PNG, JPEG, WebP, or PDF, and the documentation lists the options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Does a secure headers test replace a penetration test?

No. It checks selected HTTP configuration signals. It does not establish that application logic, dependencies, authentication, authorization, or infrastructure are secure.

Should every site use every header?

No. Policies must match the site’s routes, resources, embeds, browser features, and supported clients.

Is a missing header always a vulnerability?

Not necessarily. Confirm the response path and understand the policy’s purpose before changing production behavior.

Why test APIs separately?

APIs often have different middleware, content types, authentication, and error handlers. A homepage result may not describe them.

What should be saved for an audit trail?

Save the exact URL, timestamp, status, redirect chain, relevant headers, scanner scope, and the policy decision made for each finding.