security-headers

v2026.09.25

Add security headers (CSP, HSTS, X-Frame-Options, Permissions-Policy) to prevent clickjacking, XSS, and data leaks. Provides copy-paste middleware for Express, Next.js, Cloudflare Workers, and Laravel. Use when hardening HTTP responses, fixing security scanner findings, or setting up a new project.

GitHub
Install command
npx skhub add afu-it/security-headers
Markdown
SKILL.md

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

HeaderWhat it preventsValue
Strict-Transport-SecurityDowngrade attacks, SSL strippingmax-age=31536000; includeSubDomains; preload
Content-Security-PolicyXSS, data injection, clickjackingSee CSP section below
X-Content-Type-OptionsMIME sniffing attacksnosniff
X-Frame-OptionsClickjackingDENY or SAMEORIGIN
Referrer-PolicyLeaking URLs to third partiesstrict-origin-when-cross-origin
Permissions-PolicyUnauthorized access to device APIscamera=(), microphone=(), geolocation=()
X-XSS-ProtectionLegacy 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).

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.25

Published

Sep 25, 2026

Category

Uncategorized

License

Not specified

Source path

skills/security-headers

Default branch

main

Latest commit

6aeeb8f

Tree SHA

14cb46d