Missing Security Headers | Blue Frog Docs

Missing Security Headers

Implement essential security headers to protect against XSS, clickjacking, and other web attacks

Missing Security Headers

What This Means

Security headers are HTTP response headers that tell browsers how to behave when handling your website's content, protecting against common attacks like cross-site scripting (XSS), clickjacking, code injection, and data theft. When security headers are missing or misconfigured, your site is vulnerable to attacks that can steal user data, deface your website, redirect users to malicious sites, inject malicious code, and damage your reputation.

Essential Security Headers

Basic Security Headers:

X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
Permissions-Policy: geolocation=(), microphone=(), camera=()

What Each Header Does:

  • X-Frame-Options - Prevents clickjacking attacks
  • X-Content-Type-Options - Stops MIME-type sniffing
  • X-XSS-Protection - Enables XSS filter (legacy browsers)
  • Referrer-Policy - Controls referrer information
  • Content-Security-Policy - Prevents XSS and code injection
  • Strict-Transport-Security - Forces HTTPS connections
  • Permissions-Policy - Controls browser features

Impact on Your Business

Security Risks Without Headers:

  • XSS attacks - Hackers inject malicious scripts
  • Clickjacking - Users tricked into clicking hidden elements
  • Data theft - Sensitive information stolen
  • Session hijacking - User accounts compromised
  • Code injection - Malware inserted into pages
  • Reputation damage - Site flagged as unsafe

Business Consequences:

  • Customer trust loss - Users avoid "unsafe" sites
  • SEO penalties - Google penalizes insecure sites
  • Legal liability - GDPR/PCI DSS compliance failures
  • Financial losses - Data breaches cost $4.24M average
  • Brand damage - News of security breach spreads
  • Downtime - Cleaning up after attacks takes time

Real-World Stats:

  • 43% of cyberattacks target small businesses
  • Sites without security headers are 5x more likely to be compromised
  • Average cost of data breach: $4.24 million
  • 60% of small businesses close within 6 months of cyber attack

How to Diagnose

Method 1: SecurityHeaders.com

  1. Visit SecurityHeaders.com
  2. Enter your domain
  3. Click Scan

Check Score:

Grade F (Bad):
❌ X-Frame-Options: Missing
❌ X-Content-Type-Options: Missing
❌ Content-Security-Policy: Missing
❌ Strict-Transport-Security: Missing

Grade A+ (Good):
✅ X-Frame-Options: SAMEORIGIN
✅ X-Content-Type-Options: nosniff
✅ Content-Security-Policy: default-src 'self'
✅ Strict-Transport-Security: max-age=31536000

Method 2: Browser DevTools

  1. Open DevTools (F12)
  2. Go to Network tab
  3. Reload page
  4. Click main document
  5. Check HeadersResponse Headers

Look For:

❌ MISSING headers:
(No security headers present)

✅ GOOD headers:
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
content-security-policy: default-src 'self'

Method 3: curl Command

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

# Output:
HTTP/2 200
x-frame-options: DENY
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
content-security-policy: default-src 'self'
# ✅ Headers present

# Or if missing:
HTTP/2 200
content-type: text/html
# ❌ No security headers

Method 4: Mozilla Observatory

  1. Visit Mozilla Observatory
  2. Enter your domain
  3. Click Scan Me

Review Results:

Score: 35/100 (F)
- Content Security Policy not implemented: -25
- X-Frame-Options not implemented: -20
- Strict-Transport-Security not implemented: -20

Method 5: Lighthouse Security Audit

  1. Open DevTools (F12)
  2. Go to Lighthouse tab
  3. Select Best Practices
  4. Run audit

Check For:

⚠️ Does not use HTTPS
⚠️ Includes front-end JavaScript libraries with known vulnerabilities
⚠️ Browser errors logged to console

General Fixes

Fix 1: Add All Essential Headers

Nginx configuration:

# nginx.conf or site config
server {
    listen 443 ssl http2;
    server_name example.com;

    # Prevent clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Prevent MIME-type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Enable XSS filter (legacy browsers)
    add_header X-XSS-Protection "1; mode=block" always;

    # Referrer policy
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Force HTTPS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Content Security Policy
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'" always;

    # Permissions Policy
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # Rest of your config...
}

Apache (.htaccess):

<IfModule mod_headers.c>
    # Prevent clickjacking
    Header always set X-Frame-Options "SAMEORIGIN"

    # Prevent MIME-type sniffing
    Header always set X-Content-Type-Options "nosniff"

    # Enable XSS filter
    Header always set X-XSS-Protection "1; mode=block"

    # Referrer policy
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    # Force HTTPS
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

    # Content Security Policy
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"

    # Permissions Policy
    Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>

Fix 2: Configure Content Security Policy (CSP)

Start restrictive, gradually open:

# Level 1: Very strict (might break things)
Content-Security-Policy: default-src 'self'

# Level 2: Allow inline styles/scripts (testing phase)
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'

# Level 3: Add external resources as needed
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.google-analytics.com https://cdn.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com

# Use report-only mode for testing
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

CSP Directives Explained:

default-src 'self'           # Default policy for all resources
script-src 'self' domain.com # Where scripts can load from
style-src 'self'             # Where styles can load from
img-src 'self' data: https:  # Where images can load from
font-src 'self'              # Where fonts can load from
connect-src 'self'           # AJAX/WebSocket connections
frame-src 'self'             # iframe sources
frame-ancestors 'self'       # Who can embed this page
object-src 'none'            # Disable plugins (Flash, Java)
base-uri 'self'              # Restrict <base> tag
form-action 'self'           # Where forms can submit to
upgrade-insecure-requests    # Upgrade HTTP to HTTPS

Fix 3: Strict-Transport-Security (HSTS)

Force HTTPS for all connections:

# Basic HSTS
Strict-Transport-Security: max-age=31536000

# Include subdomains
Strict-Transport-Security: max-age=31536000; includeSubDomains

# Preload (submit to browser HSTS list)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Submit to HSTS Preload List:

  1. Visit hstspreload.org
  2. Enter your domain
  3. Check eligibility
  4. Submit for preloading
  5. Wait for inclusion (weeks to months)

Fix 4: Node.js/Express with Helmet

Use Helmet middleware:

const express = require('express');
const helmet = require('helmet');

const app = express();

// Apply all helmet defaults
app.use(helmet());

// OR configure individually
app.use(helmet({
    // Prevent clickjacking
    frameguard: { action: 'sameorigin' },

    // Prevent MIME-type sniffing
    contentTypeOptions: { nosniff: true },

    // HSTS
    hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
    },

    // CSP
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", "https://www.google-analytics.com"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            imgSrc: ["'self'", "data:", "https:"],
            fontSrc: ["'self'"],
            connectSrc: ["'self'"],
            frameAncestors: ["'self'"]
        }
    },

    // Referrer policy
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

app.get('/', (req, res) => {
    res.send('Hello with security headers!');
});

Fix 5: WordPress Security Headers

Using plugin (easy way):

  1. Install WP Security Headers plugin
  2. Go to SettingsSecurity Headers
  3. Enable recommended headers
  4. Save changes

Manual (functions.php or custom plugin):

add_action('send_headers', 'add_security_headers');
function add_security_headers() {
    // Prevent clickjacking
    header('X-Frame-Options: SAMEORIGIN');

    // Prevent MIME-type sniffing
    header('X-Content-Type-Options: nosniff');

    // XSS protection
    header('X-XSS-Protection: 1; mode=block');

    // Referrer policy
    header('Referrer-Policy: strict-origin-when-cross-origin');

    // HSTS (if using HTTPS)
    if (is_ssl()) {
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
    }

    // CSP (adjust as needed)
    header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'");
}

Fix 6: Cloudflare Security Headers

Using Transform Rules:

  1. Cloudflare DashboardRulesTransform Rules
  2. HTTP Response Header Modification
  3. Create new rule:
Rule name: Security Headers

When incoming requests match:
- All incoming requests

Then:
- Set static header: X-Frame-Options = SAMEORIGIN
- Set static header: X-Content-Type-Options = nosniff
- Set static header: X-XSS-Protection = 1; mode=block
- Set static header: Referrer-Policy = strict-origin-when-cross-origin
- Set static header: Permissions-Policy = geolocation=(), microphone=(), camera=()

Using Workers:

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
    const response = await fetch(request);
    const newHeaders = new Headers(response.headers);

    // Add security headers
    newHeaders.set('X-Frame-Options', 'SAMEORIGIN');
    newHeaders.set('X-Content-Type-Options', 'nosniff');
    newHeaders.set('X-XSS-Protection', '1; mode=block');
    newHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin');
    newHeaders.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    newHeaders.set('Content-Security-Policy', "default-src 'self'");
    newHeaders.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');

    return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: newHeaders
    });
}

Fix 7: Netlify/Vercel Headers

Netlify (_headers file in root):

/*
  X-Frame-Options: SAMEORIGIN
  X-Content-Type-Options: nosniff
  X-XSS-Protection: 1; mode=block
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com
  Permissions-Policy: geolocation=(), microphone=(), camera=()

Vercel (vercel.json):

{
    "headers": [
        {
            "source": "/(.*)",
            "headers": [
                {
                    "key": "X-Frame-Options",
                    "value": "SAMEORIGIN"
                },
                {
                    "key": "X-Content-Type-Options",
                    "value": "nosniff"
                },
                {
                    "key": "X-XSS-Protection",
                    "value": "1; mode=block"
                },
                {
                    "key": "Referrer-Policy",
                    "value": "strict-origin-when-cross-origin"
                },
                {
                    "key": "Strict-Transport-Security",
                    "value": "max-age=31536000; includeSubDomains"
                },
                {
                    "key": "Content-Security-Policy",
                    "value": "default-src 'self'"
                },
                {
                    "key": "Permissions-Policy",
                    "value": "geolocation=(), microphone=(), camera=()"
                }
            ]
        }
    ]
}

Fix 8: Testing CSP Without Breaking Site

Use report-only mode:

# Test CSP without blocking anything
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

# CSP violations will be reported but not blocked

Create reporting endpoint:

// Node.js CSP report handler
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
    console.log('CSP Violation:', req.body);
    // Log to monitoring system
    res.status(204).end();
});

Platform-Specific Guides

Detailed implementation instructions for your specific platform:

Platform Troubleshooting Guide
Shopify Shopify Security Headers Guide
WordPress WordPress Security Headers Guide
Wix Wix Security Headers Guide
Squarespace Squarespace Security Headers Guide
Webflow Webflow Security Headers Guide

Verification

After implementing security headers:

Test 1: SecurityHeaders.com

  1. Visit SecurityHeaders.com
  2. Enter your domain
  3. Should get A or A+ grade

Test 2: curl Check

curl -I https://example.com

# Should show all headers:
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
content-security-policy: default-src 'self'

Test 3: Browser DevTools

  1. Network tab → Headers
  2. Verify all security headers present
  3. No console errors from CSP blocks

Test 4: Mozilla Observatory

  1. Scan site
  2. Score should be 70+
  3. No critical issues

Common Mistakes

  1. CSP too strict - Breaks functionality, use report-only first
  2. Missing 'always' in Nginx - Headers only sent on 200 responses
  3. Conflicting headers - Multiple X-Frame-Options values
  4. Not testing thoroughly - Site breaks after deployment
  5. Forgetting subdomains - HSTS includeSubDomains needed
  6. HSTS without HTTPS - Must have valid SSL first
  7. Hardcoding domains in CSP - Use variables for environments
  8. Not monitoring CSP reports - Miss violations

Further Reading

// SYS.FOOTER