Security (as discussed in OWASP ZAP Automation: Security Scanning in CI/CD) headers are HTTP response headers that enhance web application security by instructing browsers how to behave when handling your site’s content. Proper implementation protects against common attacks like XSS (as discussed in Penetration Testing Basics for QA Testers), clickjacking, and man-in-the-middle attacks.

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"

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