H Heygents Docs Open App

Updated July 31, 2026 · Security · Medium severity

No HTTP security headers served (no HSTS, X-Frame-Options, or CSP)

why is my site missing HTTP security headers like HSTS and X-Frame-Options Most likely because neither your reverse proxy nor your app framework sets them, which is the default state for a fresh nginx or Caddy config. Add Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options or frame-ancestors, Referrer-Policy, and Permissions-Policy at the proxy layer. Confirm with `curl -sI https://example.com`.

Seen in 9 of 450 scanned projects (2%). Based on curl -sI header scans across 450 audited production deployments.

Common stacks: nginx caddy node express security-headers

How to tell you have it

  • curl -sI shows no Strict-Transport-Security header on an HTTPS response
  • Browser devtools security panel flags missing headers
  • Site can be embedded in an iframe on an unrelated domain
  • Third-party scanners (securityheaders.com style tools) report grade F or D
  • MIME sniffing warnings in browser console on older browsers

Why it matters

Without X-Frame-Options or a frame-ancestors CSP directive, any external site can load your pages inside an iframe. That opens the door to clickjacking, where an attacker overlays invisible buttons on top of your real UI and tricks a logged-in user into clicking something they never intended, like changing an email address or approving a payment.

Without Strict-Transport-Security, a browser that has never visited your site over HTTPS will happily follow an http:// link first, giving an on-path attacker a window to downgrade the connection or strip TLS entirely before the user notices. HSTS closes that window by telling the browser to never even attempt plain HTTP again.

Without X-Content-Type-Options: nosniff, older or misconfigured browsers may MIME-sniff a response and execute a file as a different content type than the server intended, for example treating an uploaded image as HTML or JavaScript. Combined with any user-generated upload path, this turns a content bug into a stored XSS vector.

Without Referrer-Policy and Permissions-Policy, you leak more than you mean to: full URLs (including query strings that sometimes carry tokens or session identifiers) get sent as the Referer header to every third-party script and image on the page, and browser features like camera, microphone, and geolocation stay available to any script running in your origin, including compromised ad or analytics tags.

How Heygents detects it

Deep Scan runs curl -sI against the live URL and checks the response headers for Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options or a Content-Security-Policy frame-ancestors directive, Referrer-Policy, and Permissions-Policy. It also inspects the nginx or Caddy config files in the project's own repo or /etc paths for existing add_header or header directives so it can tell whether headers are missing entirely or only missing on certain routes.

How to fix it

  1. Confirm which headers are actually missing Run curl against the live HTTPS URL and read the response headers directly rather than guessing. Do this before and after any change so you have a clear before/after diff.
  2. Set headers at the reverse proxy with nginx Add these inside the server block that terminates TLS. This covers every route behind the proxy in one place, including static assets and error pages, without touching app code.
  3. Set headers at the reverse proxy with Caddy Caddy uses a header block instead of add_header. This goes inside the site block for the domain, and applies to every response Caddy serves for that host.
  4. Set headers in the app layer if you do not control the proxy If the app sits behind a managed load balancer or CDN you cannot edit, set headers in application middleware instead. For an Express app this covers every response the app itself generates.
  5. Reload the proxy and re-verify Config changes to nginx or Caddy do not take effect until you test and reload. Re-run the curl check afterward and confirm every header now shows up on the live response, not just in a local test.

Confirm which headers are actually missing

curl -sI https://example.com/ | grep -Ei 'strict-transport|x-content-type|x-frame|referrer-policy|permissions-policy'

Set headers at the reverse proxy with nginx

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Set headers at the reverse proxy with Caddy

example.com {
  reverse_proxy localhost:3000
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains"
    X-Content-Type-Options "nosniff"
    X-Frame-Options "SAMEORIGIN"
    Referrer-Policy "strict-origin-when-cross-origin"
    Permissions-Policy "camera=(), microphone=(), geolocation=()"
  }
}

Set headers in the app layer if you do not control the proxy

app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
});

Reload the proxy and re-verify

sudo nginx -t && sudo systemctl reload nginx
curl -sI https://example.com/ | grep -Ei 'strict-transport|x-content-type|x-frame|referrer-policy|permissions-policy'

Find this in your own projects, automatically

Heygents runs a read-only Deep Scan across every project you own, finds issues like this one, and hands you a ready-to-run fix an AI agent can execute and verify. A solo developer gets the audit, the backlog and the fix loop in one place.

Open Heygents →

Frequently asked questions

Should I set security headers in nginx/Caddy or in my app code?

Prefer the reverse proxy when you control it. One config change covers every route, including static files and error pages the app framework never touches, and it survives app redeploys and framework migrations untouched. Fall back to app middleware only when you sit behind a managed CDN or load balancer where you cannot edit the proxy layer directly.

Is HSTS preload safe to turn on right away?

Not immediately. Preloading gets your domain baked into browser source code, and removal takes months to propagate once you are listed. Run HSTS with a normal max-age first, confirm every subdomain genuinely serves valid HTTPS, and only add the preload directive and submit to the preload list once you are certain you will never need to serve plain HTTP again.

What is the difference between X-Frame-Options and frame-ancestors in CSP?

X-Frame-Options is the older, simpler header supported by essentially every browser, but it only allows DENY, SAMEORIGIN, or a single origin. The frame-ancestors directive inside Content-Security-Policy is more flexible, supporting multiple allowed origins, and is the modern replacement. Setting both together is the safest path since it covers browsers that only understand one or the other.

Why does curl -sI show no headers even though nginx has add_header lines?

The most common cause is that add_header only applies to responses generated at the level it is declared, and it gets silently dropped if a nested location block or an included file has its own add_header directives without repeating the parent ones. Add the missing headers directly inside the specific location block, or move them above any competing add_header lines, then reload and retest.