Overview

Add essential security headers to all responses.

Features

  • Content-Security-Policy
  • Strict-Transport-Security
  • X-Frame-Options
  • Permissions-Policy

Installation

npx hanma add elysia-security-headers --framework elysia

Code

import { Elysia } from "elysia";

export interface SecurityHeadersOptions {
  /** Content Security Policy directives */
  contentSecurityPolicy?: Record<string, string | string[]> | false;
  /** X-Content-Type-Options (default: nosniff) */
  contentTypeOptions?: string | false;
  /** X-Frame-Options (default: DENY) */
  frameOptions?: "DENY" | "SAMEORIGIN" | false;
  /** Strict-Transport-Security options */
  hsts?: {
    maxAge?: number;
    includeSubDomains?: boolean;
    preload?: boolean;
  } | false;
  /** X-XSS-Protection (default: 0, as it's deprecated) */
  xssProtection?: string | false;
  /** Referrer-Policy (default: strict-origin-when-cross-origin) */
  referrerPolicy?: string | false;
  /** Permissions-Policy directives */
  permissionsPolicy?: Record<string, string[]> | false;
}

const buildCsp = (directives: Record<string, string | string[]>): string => {
  return Object.entries(directives)
    .map(([key, value]) => {
      const values = Array.isArray(value) ? value.join(" ") : value;
      return `${key} ${values}`;
    })
    .join("; ");
};

const buildPermissionsPolicy = (directives: Record<string, string[]>): string => {
  return Object.entries(directives)
    .map(([key, value]) => `${key}=(${value.join(" ")})`)
    .join(", ");
};

/**
 * Security headers middleware for Elysia
 * Adds common security headers to all responses
 */
export const securityHeaders = (options: SecurityHeadersOptions = {}) => {
  const isProd = process.env.NODE_ENV === "production";

  const config = {
    contentSecurityPolicy: options.contentSecurityPolicy ?? {
      "default-src": "'self'",
      "script-src": ["'self'", isProd ? "" : "'unsafe-inline'"].filter(Boolean),
      "style-src": ["'self'", "'unsafe-inline'"],
      "img-src": ["'self'", "data:", "https:"],
      "font-src": ["'self'"],
      "connect-src": ["'self'"],
      "frame-ancestors": "'none'",
    },
    contentTypeOptions: options.contentTypeOptions ?? "nosniff",
    frameOptions: options.frameOptions ?? "DENY",
    hsts: options.hsts ?? (isProd ? { maxAge: 31536000, includeSubDomains: true } : false),
    xssProtection: options.xssProtection ?? "0",
    referrerPolicy: options.referrerPolicy ?? "strict-origin-when-cross-origin",
    permissionsPolicy: options.permissionsPolicy ?? {
      camera: [],
      microphone: [],
      geolocation: [],
    },
  };

  return new Elysia({ name: "security-headers" })
    .onAfterHandle(({ set }) => {
      // Content-Security-Policy
      if (config.contentSecurityPolicy !== false) {
        set.headers["Content-Security-Policy"] = buildCsp(config.contentSecurityPolicy);
      }

      // X-Content-Type-Options
      if (config.contentTypeOptions !== false) {
        set.headers["X-Content-Type-Options"] = config.contentTypeOptions;
      }

      // X-Frame-Options
      if (config.frameOptions !== false) {
        set.headers["X-Frame-Options"] = config.frameOptions;
      }

      // Strict-Transport-Security
      if (config.hsts !== false) {
        let hstsValue = `max-age=${config.hsts.maxAge ?? 31536000}`;
        if (config.hsts.includeSubDomains) hstsValue += "; includeSubDomains";
        if (config.hsts.preload) hstsValue += "; preload";
        set.headers["Strict-Transport-Security"] = hstsValue;
      }

      // X-XSS-Protection (deprecated but still used)
      if (config.xssProtection !== false) {
        set.headers["X-XSS-Protection"] = config.xssProtection;
      }

      // Referrer-Policy
      if (config.referrerPolicy !== false) {
        set.headers["Referrer-Policy"] = config.referrerPolicy;
      }

      // Permissions-Policy
      if (config.permissionsPolicy !== false) {
        set.headers["Permissions-Policy"] = buildPermissionsPolicy(config.permissionsPolicy);
      }
    });
};

/* USAGE:

import { Elysia } from "elysia";
import { securityHeaders } from "./middleware/security-headers";

// Basic usage with defaults
const app = new Elysia()
  .use(securityHeaders())
  .get("/", () => "Hello World")
  .listen(3000);

// With custom options
const app = new Elysia()
  .use(securityHeaders({
    frameOptions: "SAMEORIGIN",
    hsts: {
      maxAge: 63072000, // 2 years
      includeSubDomains: true,
      preload: true,
    },
    contentSecurityPolicy: {
      "default-src": "'self'",
      "script-src": ["'self'", "https://cdn.example.com"],
      "img-src": ["'self'", "https:", "data:"],
    },
  }))
  .listen(3000);

*/

Usage

import { securityHeaders } from './middleware/security-headers';

app.use(securityHeaders());

Dependencies

No external dependencies required.

Frequently Asked Questions

How do I install Security Headers?
Run the following command: npx hanma add elysia-security-headers --framework elysia
How do I use this snippet?
Copy the code and paste it into your project. The snippet will be saved to src/middleware/security-headers.ts.
Can I customize this snippet?
Yes! All snippets are copy-paste code that you own. Feel free to modify them to fit your needs.
Does this work with other frameworks?
This snippet is designed for the specific framework. Check the other framework pages for equivalent implementations.