When we audit web applications, the first thing we check is not the code. It is the HTTP response headers. They take seconds to inspect, and they immediately reveal how much thought an engineering team has put into security.
In our recent scan of 50 companies, missing or misconfigured security headers were the single most common finding -- present across every grade from B through F. These are not obscure best practices. They are browser-native defense mechanisms that have been available for years, are free to implement, and take less than an hour to configure correctly.
Here are the five headers that most SaaS companies get wrong, what they do, and how to fix them.
1. Strict-Transport-Security (HSTS)
What It Does
HSTS tells browsers to only connect to your site over HTTPS. Once a browser receives this header, it will automatically upgrade any HTTP request to HTTPS for the duration specified in the max-age directive -- even if the user types http:// in the address bar or clicks an HTTP link.
Why It Matters
Without HSTS, a user on a public WiFi network can be subjected to an SSL stripping attack. The attacker intercepts the initial HTTP request (before the redirect to HTTPS) and serves the site over HTTP, proxying requests to the real HTTPS backend. The user sees no lock icon but often does not notice. Every credential, session cookie, and piece of data flows through the attacker in plaintext.
The Correct Configuration
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Breaking this down:
max-age=31536000-- enforce HTTPS for 1 year (the minimum recommended value for production)includeSubDomains-- apply the policy to all subdomains (prevents an attacker from exploiting an insecure subdomain to set cookies that affect the parent domain)preload-- opt in to the HSTS preload list maintained by browsers, which hardcodes your domain as HTTPS-only even on first visit
Common Mistakes
- max-age too short: Values like
max-age=86400(1 day) provide almost no protection. If a user does not visit your site for 24 hours, the HSTS directive expires and they are vulnerable again. - Missing includeSubDomains: Without this, a subdomain like
staging.yourapp.comcan be loaded over HTTP, which can be used to set cookies that override those on the main domain. - Deployed on HTTP: HSTS headers sent over HTTP are ignored by browsers (for obvious reasons -- an attacker could inject a fake HSTS header). The header must be served over a valid HTTPS connection.
How to Test
curl -sI https://yourdomain.com | grep -i strict-transport
Or use SecurityHeaders.com for a comprehensive scan.
2. Content-Security-Policy (CSP)
What It Does
CSP is the most powerful security header available. It tells the browser exactly which sources of content are allowed on your page -- which domains can serve JavaScript, CSS, images, fonts, frames, and other resources. If an attacker manages to inject a script tag (XSS), CSP can prevent the injected script from executing because it does not match the allowed sources.
Why It Matters
Cross-site scripting (XSS) remains one of the most common web vulnerabilities. A strong CSP is the most effective browser-side mitigation against XSS exploitation. Without it, any XSS vulnerability becomes trivially exploitable -- the attacker's injected script can load external resources, exfiltrate data, and modify the DOM without restriction.
The Problem: Most CSPs Are Useless
The vast majority of CSP implementations we encounter are so permissive that they provide no meaningful security. Common patterns we see:
# This CSP does almost nothing:
Content-Security-Policy: default-src *; script-src * 'unsafe-inline' 'unsafe-eval'
# This is only slightly better:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
The 'unsafe-inline' directive is the most common offender. It allows inline scripts and event handlers, which is exactly what most XSS payloads use. A CSP with 'unsafe-inline' in the script-src directive blocks almost no XSS attacks.
A Better Approach
Use nonce-based CSP instead of allowing 'unsafe-inline':
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self' 'nonce-{random}'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
Generate a unique nonce for each page load and add it to every legitimate script tag: <script nonce="{random}">. Injected scripts will not have the nonce and will be blocked.
Deployment Strategy
Start with report-only mode to avoid breaking your application:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
Monitor the reports, add legitimate sources to the allowlist, and switch to enforcement when violations drop to zero for legitimate traffic.
3. X-Frame-Options
What It Does
X-Frame-Options controls whether your page can be embedded in an iframe. This is the primary defense against clickjacking attacks, where an attacker overlays your page with a transparent iframe and tricks users into clicking on elements they cannot see.
The Correct Configuration
X-Frame-Options: DENY
If your application legitimately needs to be framed (for example, embedded widgets), use:
X-Frame-Options: SAMEORIGIN
For more granular control, use CSP's frame-ancestors directive instead, which supports allowlisting specific domains:
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com
Common Mistakes
- Not setting it at all: The default behavior allows any page to iframe yours.
- Using ALLOW-FROM: The
ALLOW-FROMdirective was never widely supported (Chrome never implemented it) and is now deprecated. Use CSPframe-ancestorsinstead. - Inconsistent application: Setting the header on some pages but not others. Every page that performs any action should be protected.
4. X-Content-Type-Options
What It Does
This header prevents browsers from MIME-sniffing a response away from the declared Content-Type. There is exactly one valid value:
X-Content-Type-Options: nosniff
Why It Matters
Without this header, a browser might interpret a file based on its content rather than its declared type. An attacker who can upload a file with a .jpg extension but containing JavaScript could trick the browser into executing it as a script if the file is loaded in certain contexts. With nosniff set, the browser strictly respects the declared Content-Type and refuses to execute content that does not match.
Common Mistakes
The only mistake is not setting it. This header has no configuration options, no edge cases, and no risk of breaking functionality. There is no reason for any application to omit it.
5. Referrer-Policy
What It Does
Referrer-Policy controls how much URL information the browser includes in the Referer header when a user navigates from your site to an external link. By default, browsers send the full URL including the path and query string.
Why It Matters
URLs often contain sensitive information: session tokens in query parameters, internal path structures, user IDs, search queries, and other data that should not be leaked to third parties. When a user clicks an external link on your page, the destination site receives the full URL of the page they came from.
Consider a URL like https://app.example.com/users/12345/settings?token=abc123. Without a restrictive Referrer-Policy, any external link on that page sends the full URL -- including the user ID and token -- to the linked site.
The Correct Configuration
Referrer-Policy: strict-origin-when-cross-origin
This sends only the origin (scheme + host) for cross-origin requests while preserving the full URL for same-origin navigation. For maximum privacy:
Referrer-Policy: no-referrer
Common Mistakes
- Not setting it: The browser default varies. Chromium defaults to
strict-origin-when-cross-origin, but older browsers may send full referrer information. - Using
unsafe-url: This explicitly sends full URLs on all requests, including cross-origin. There is almost never a legitimate reason for this.
The Complete Header Set
Here is the complete set of security headers we recommend for every SaaS application. Copy this into your web server configuration and adjust the CSP to match your application's needs:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Verification Tools
After deploying these headers, verify them with these free tools:
- SecurityHeaders.com -- grades your headers from A+ to F and explains what is missing
- Mozilla Observatory -- comprehensive web security scan including headers, TLS, cookies, and more
- Browser DevTools -- open the Network tab, click any request, and inspect the Response Headers section
Aim for an A+ on SecurityHeaders.com. It is achievable for any application and takes less than an afternoon of work.
How do your headers stack up?
Our free scan checks your security headers, DMARC, TLS configuration, and more in minutes.
Scan Your Domain Free