Security headers are HTTP response headers that instruct browsers to enforce security policies, protecting web applications against XSS, clickjacking, MIME type sniffing, and other common attacks with minimal implementation effort. According to the Mozilla Web Security Observatory, only 35% of websites properly implement Content Security Policy (CSP), and 45% are missing HTTP Strict Transport Security (HSTS), leaving millions of users vulnerable to preventable attacks. According to OWASP, missing or misconfigured security headers are responsible for approximately 30% of web application security vulnerabilities found during penetration testing. For QA engineers and developers, automated security header testing with tools like Mozilla Observatory, SecurityHeaders.com, or custom pytest/Playwright scripts provides fast, reproducible validation that security configurations remain correct across deployments.

TL;DR: Test 8 critical security headers: Content-Security-Policy, Strict-Transport-Security (HSTS), X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Resource-Policy, and Cross-Origin-Opener-Policy. Use Mozilla Observatory, SecurityHeaders.com, or automated tests in your CI/CD pipeline. Aim for A+ grade on Mozilla Observatory.

Essential Security Headers

1. Content-Security-Policy (CSP)

Purpose: Prevents XSS attacks by controlling resource loading

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none';

Testing:

def test_csp_header():
    response = requests.get("https://example.com")
    csp = response.headers.get('Content-Security-Policy')

    assert csp is not None, "CSP header missing"
    assert "default-src 'self'" in csp, "CSP too permissive"
    assert "'unsafe-eval'" not in csp, "unsafe-eval should be avoided"

2. Strict-Transport-Security (HSTS)

Purpose: Forces HTTPS connections

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Testing:

def test_hsts_header():
    response = requests.get("https://example.com")
    hsts = response.headers.get('Strict-Transport-Security')

 (as discussed in [Security Testing for QA: A Practical Guide](/blog/security-testing-for-qa))    assert hsts is not None, "HSTS header missing"
    assert 'max-age=' in hsts, "max-age directive missing"

    # Extract max-age value
    max_age = int(hsts.split('max-age=')[1].split(';')[0])
    assert max_age >= 31536000, "max-age should be at least 1 year"
    assert 'includeSubDomains' in hsts, "Should include subdomains"

3. X-Frame-Options

Purpose: Prevents clickjacking attacks

X-Frame-Options: DENY
# or
X-Frame-Options: SAMEORIGIN

Testing:

def test_x_frame_options():
    response = requests.get("https://example.com")
    xfo = response.headers.get('X-Frame-Options')

    assert xfo is not None, "X-Frame-Options missing"
    assert xfo in ['DENY', 'SAMEORIGIN'], f"Invalid value: {xfo}"

4. X-Content-Type-Options

Purpose: Prevents MIME-sniffing

X-Content-Type-Options: nosniff

Testing:

def test_x_content_type_options():
    response = requests.get("https://example.com")
    xcto = response.headers.get('X-Content-Type-Options')

    assert xcto == 'nosniff', "X-Content-Type-Options should be 'nosniff'"

5. X-XSS-Protection

Purpose: Enables browser XSS protection (legacy)

X-XSS-Protection: 1; mode=block

Note: Deprecated in favor of CSP, but still useful for older browsers

6. Referrer-Policy

Purpose: Controls referrer information

Referrer-Policy: strict-origin-when-cross-origin

Testing:

def test_referrer_policy():
    response = requests.get("https://example.com")
    rp = response.headers.get('Referrer-Policy')

    allowed_values = [
        'no-referrer',
        'no-referrer-when-downgrade',
        'origin',
        'origin-when-cross-origin',
        'same-origin',
        'strict-origin',
        'strict-origin-when-cross-origin'
    ]

    assert rp in allowed_values, f"Invalid Referrer-Policy: {rp}"

7. Permissions-Policy

Purpose: Controls browser features and APIs

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

Testing:

def test_permissions_policy():
    response = requests.get("https://example.com")
    pp = response.headers.get('Permissions-Policy')

    assert pp is not None, "Permissions-Policy missing"
    assert 'geolocation=()' in pp, "Should disable geolocation"
    assert 'camera=()' in pp, "Should disable camera"

“Security headers are the easiest security wins a web team can implement — CSP, HSTS, and X-Frame-Options take an hour to configure but protect against entire classes of attacks that take years to discover in code.” — Yuri Kan, Senior QA Lead

Comprehensive Security Headers Test Suite

import requests
import pytest

class TestSecurityHeaders:
    base_url = "https://example.com"

    def test_all_security_headers(self):
        response = requests.get(self.base_url)
        headers = response.headers

        # Required headers
        required_headers = {
            'Content-Security-Policy': self.validate_csp,
            'Strict-Transport-Security': self.validate_hsts,
            'X-Frame-Options': self.validate_xfo,
            'X-Content-Type-Options': self.validate_xcto,
            'Referrer-Policy': self.validate_referrer,
        }

        for header_name, validator in required_headers.items():
            assert header_name in headers, f"{header_name} missing"
            validator(headers[header_name])

    def validate_csp(self, value):
        assert "default-src" in value
        assert "'unsafe-eval'" not in value

    def validate_hsts(self, value):
        assert "max-age=" in value
        max_age = int(value.split('max-age=')[1].split(';')[0])
        assert max_age >= 15768000  # 6 months minimum

    def validate_xfo(self, value):
        assert value in ['DENY', 'SAMEORIGIN']

    def validate_xcto(self, value):
        assert value == 'nosniff'

    def validate_referrer(self, value):
        allowed = [
            'no-referrer', 'strict-origin',
            'strict-origin-when-cross-origin'
        ]
        assert value in allowed

    def test_no_information_disclosure(self):
        response = requests.get(self.base_url)
        headers = response.headers

        # Should not expose server information
        if 'Server' in headers:
            server = headers['Server'].lower()
            assert 'apache' not in server, "Server version exposed"
            assert 'nginx' not in server, "Server version exposed"

        # Should not expose technology stack
        assert 'X-Powered-By' not in headers, "X-Powered-By should be removed"
        assert 'X-AspNet-Version' not in headers, "ASP.NET version exposed"

Automated Testing Tools

1. SecurityHeaders.com

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

2. Observatory by Mozilla

import requests

def test_mozilla_observatory():
    url = "https://http-observatory.security.mozilla.org/api/v1/analyze"
    params = {"host": "example.com"}

    response = requests.post(url, params=params)
    scan_id = response.json()['scan_id']

    # Wait for scan to complete
    result_url = f"https://http-observatory.security.mozilla.org/api/v1/getScanResults?scan={scan_id}"
    result = requests.get(result_url).json()

    assert result['grade'] in ['A+', 'A', 'B'], f"Security grade: {result['grade']}"

3. Custom Script

#!/bin/bash
# security-headers-check.sh

URL=$1

echo "Checking security headers for: $URL"

headers=(
  "Content-Security-Policy"
  "Strict-Transport-Security"
  "X-Frame-Options"
  "X-Content-Type-Options"
  "Referrer-Policy"
)

for header in "${headers[@]}"; do
  value=$(curl -s -I "$URL" | grep -i "^$header:" | cut -d' ' -f2-)
  if [ -z "$value" ]; then
    echo "❌ $header: MISSING"
  else
    echo "✅ $header: $value"
  fi
done

Implementation Guide

Node.js/Express

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.example.com"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  frameguard: {
    action: 'deny'
  },
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin'
  }
}));

Python/Django

# settings.py
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True

X_FRAME_OPTIONS = 'DENY'

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "https://cdn.example.com")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")

Nginx

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com;" always;
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;

# Remove server version
server_tokens off;

CI/CD Integration

# .github/workflows/security-headers.yml
name: Security Headers Test

on: [push, pull_request]

jobs:
  test-headers:
    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v3

      - name: Run Security Headers Test
        run: |
          pytest tests/test_security_headers.py

      - name: Check with securityheaders.com
        run: |
          curl -s "https://securityheaders.com/?q=${{ secrets.PRODUCTION_URL }}&hide=on&followRedirects=on" | grep "Grade: A"

Conclusion

Security headers are a crucial defense layer for web applications. Regular testing ensures proper implementation and helps protect against common web vulnerabilities.

Key Takeaways:

  • Implement all essential security headers
  • Test headers in CI/CD pipeline
  • Use automated scanning tools
  • Monitor header effectiveness
  • Keep headers updated with security best practices
  • Remove information disclosure headers

FAQ

Which security headers are most critical to implement first?

Start with Strict-Transport-Security (HSTS) to enforce HTTPS, Content-Security-Policy (CSP) to prevent XSS attacks, and X-Frame-Options set to DENY to block clickjacking. These three headers protect against the most common attack vectors. Next add X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin. Aim for an A+ grade on Mozilla Observatory as your benchmark.

How do I test security headers in a CI/CD pipeline?

Add automated header checks to your CI/CD workflow using pytest with the requests library to validate each required header against expected values. Alternatively, use SecurityHeaders.com API or Mozilla Observatory API for automated grading. Run these checks against your staging environment after each deployment. Fail the pipeline if critical headers like HSTS or CSP are missing or misconfigured.

What is Content Security Policy and why is it difficult to implement?

Content Security Policy (CSP) is an HTTP header that tells browsers which sources of content are allowed to load on your page, effectively preventing XSS attacks. It is challenging because overly strict CSP breaks legitimate functionality (inline scripts, third-party widgets), while too-permissive CSP provides little protection. Start with CSP in report-only mode to identify violations without breaking your site, then progressively tighten directives.

Should I remove the X-Powered-By and Server headers?

Yes, always remove or minimize information disclosure headers. X-Powered-By reveals your technology stack (e.g., PHP, Express), and detailed Server headers expose your web server version (e.g., Apache/2.4.51). Attackers use this information to target known vulnerabilities in specific versions. In Nginx, set server_tokens off. In Express, use helmet middleware which removes X-Powered-By by default.

Official Resources

See Also