SuperAPI AI Directory

security-headers

Security & auditing

View repository

Updated 10-15 08:43

About this skill

Security Headers

Add HTTP security headers to every response. These are zero-effort, high-impact protections that prevent entire classes of attacks.

The Rule

EVERY HTTP response must include security headers. No exceptions.

Required Headers

Header What it prevents Value
Strict-Transport-Security Downgrade attacks, SSL stripping max-age=31536000; includeSubDomains; preload
Content-Security-Policy XSS, data injection, clickjacking See CSP section below
X-Content-Type-Options MIME sniffing attacks nosniff
X-Frame-Options Clickjacking DENY or SAMEORIGIN
Referrer-Policy Leaking URLs to third parties strict-origin-when-cross-origin
Permissions-Policy Unauthorized access to device APIs camera=(), microphone=(), geolocation=()
X-XSS-Protection Legacy XSS filter (older browsers) 0 (disabled -- CSP is better)

Framework Implementations

Cloudflare Workers

function addSecurityHeaders(response: Response): Response {
  const headers = new Headers(response.headers);

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

  // Remove headers that leak server info
  headers.delete('Server');
  headers.delete('X-Powered-By');

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

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const response = await handleRequest(request, env);
    return addSecurityHeaders(response);
  }
};

Express.js / Node.js

import helmet from 'helmet';

// Option 1: Use helmet (recommended)
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https:"],
      frameAncestors: ["'none'"],
    },
  },
  hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
}));

// Option 2: Manual middleware (no dependency)
app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  res.removeHeader('X-Powered-By');
  next();
});

Next.js

// next.config.js
const securityHeaders = [
  { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

Laravel / PHP

// app/Http/Middleware/SecurityHeaders.php
class SecurityHeaders
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
        $response->headers->remove('X-Powered-By');
        $response->headers->remove('Server');

        return $response;
    }
}

// Register in app/Http/Kernel.php
protected $middleware = [
    \App\Http\Middleware\SecurityHeaders::class,
    // ...
];

Content-Security-Policy (CSP) Guide

CSP is the most complex header. Start strict, loosen only when needed:

Strict (API-only, no frontend)

Content-Security-Policy: default-src 'none'; frame-ancestors 'none'

Standard (SPA with CDN assets)

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

With Third-Party Scripts (analytics, payments)

Content-Security-Policy: default-src 'self'; script-src 'self' https://js.stripe.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.stripe.com https://www.google-analytics.com; frame-src https://js.stripe.com; frame-ancestors 'none'

CSP Report-Only (test before enforcing)

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

Headers to Remove

These headers leak information about your stack:

Server: nginx/1.21.0          → Remove
X-Powered-By: Express         → Remove
X-AspNet-Version: 4.0.30319   → Remove
X-AspNetMvc-Version: 5.2      → Remove

Verification

Test your headers at:

  • Run curl -I https://yoursite.com and check response headers
  • Use browser DevTools → Network tab → Response Headers

PDPA 2024 Relevance

Security headers are part of langkah keselamatan yang munasabah (reasonable security measures) under PDPA 2024. Missing headers that lead to XSS or clickjacking attacks resulting in data breaches constitute negligence under Seksyen 5(1).