diff --git a/docs/SECURITY_HEADERS.md b/docs/SECURITY_HEADERS.md new file mode 100644 index 00000000..75a75993 --- /dev/null +++ b/docs/SECURITY_HEADERS.md @@ -0,0 +1,25 @@ +# 🛡️ HTTP Security Headers Specification + +This document details the HTTP security headers implemented in the NotifyChain API server (Issue #690). + +--- + +## 1. Configured Security Headers + +| Header | Value | Purpose | +|---|---|---| +| `X-Content-Type-Options` | `nosniff` | Blocks MIME-type sniffing | +| `X-Frame-Options` | `DENY` | Prevents clickjacking in iframes | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limits referrer leakage | +| `X-XSS-Protection` | `0` | Disables legacy XSS auditor in favor of CSP | +| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | Restricts privileged browser APIs | +| `Content-Security-Policy` | `default-src 'self'; frame-ancestors 'none';` | Restricts untrusted script/frame embedding | +| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` | Enforces HTTPS in production | + +--- + +## 2. Integration & Verification + +The middleware `applySecurityHeaders` is automatically applied on all incoming HTTP requests in `listener/src/api/events-server.ts`. + +Automated tests are located at `listener/src/middleware/security-headers.test.ts`. diff --git a/listener/src/middleware/security-headers.test.ts b/listener/src/middleware/security-headers.test.ts new file mode 100644 index 00000000..74f75b4f --- /dev/null +++ b/listener/src/middleware/security-headers.test.ts @@ -0,0 +1,55 @@ +import { IncomingMessage, ServerResponse } from 'http'; +import { applySecurityHeaders, DEFAULT_SECURITY_HEADERS, HSTS_HEADER } from './security-headers'; + +describe('Security Headers Middleware (Issue #690)', () => { + test('attaches core security headers to HTTP responses', () => { + const headersMap: Record = {}; + const req = {} as IncomingMessage; + const res = { + hasHeader: jest.fn((name: string) => name in headersMap), + setHeader: jest.fn((name: string, value: string) => { + headersMap[name] = value; + }), + } as unknown as ServerResponse; + + applySecurityHeaders(req, res, { isProduction: false }); + + expect(res.setHeader).toHaveBeenCalledWith('X-Content-Type-Options', 'nosniff'); + expect(res.setHeader).toHaveBeenCalledWith('X-Frame-Options', 'DENY'); + expect(res.setHeader).toHaveBeenCalledWith('Referrer-Policy', 'strict-origin-when-cross-origin'); + expect(res.setHeader).toHaveBeenCalledWith('X-XSS-Protection', '0'); + expect(res.setHeader).toHaveBeenCalledWith('Content-Security-Policy', expect.stringContaining("default-src 'self'")); + }); + + test('enables HSTS in production environments', () => { + const headersMap: Record = {}; + const req = {} as IncomingMessage; + const res = { + hasHeader: jest.fn((name: string) => name in headersMap), + setHeader: jest.fn((name: string, value: string) => { + headersMap[name] = value; + }), + } as unknown as ServerResponse; + + applySecurityHeaders(req, res, { isProduction: true, enableHsts: true }); + + expect(res.setHeader).toHaveBeenCalledWith('Strict-Transport-Security', HSTS_HEADER); + }); + + test('does not overwrite existing custom headers', () => { + const headersMap: Record = { + 'X-Frame-Options': 'SAMEORIGIN', + }; + const req = {} as IncomingMessage; + const res = { + hasHeader: jest.fn((name: string) => name in headersMap), + setHeader: jest.fn((name: string, value: string) => { + headersMap[name] = value; + }), + } as unknown as ServerResponse; + + applySecurityHeaders(req, res, { isProduction: false }); + + expect(res.setHeader).not.toHaveBeenCalledWith('X-Frame-Options', 'DENY'); + }); +}); diff --git a/listener/src/middleware/security-headers.ts b/listener/src/middleware/security-headers.ts new file mode 100644 index 00000000..560fce4e --- /dev/null +++ b/listener/src/middleware/security-headers.ts @@ -0,0 +1,47 @@ +/** + * Security Headers Middleware (Issue #690) + * + * Attaches industry-standard HTTP security headers to all responses from the + * NotifyChain API server to mitigate clickjacking, MIME-sniffing, and XSS attacks. + */ + +import { IncomingMessage, ServerResponse } from 'http'; + +export interface SecurityHeadersConfig { + isProduction?: boolean; + enableHsts?: boolean; + customCsp?: string; +} + +export const DEFAULT_SECURITY_HEADERS: Record = { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'X-XSS-Protection': '0', + 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()', + 'Content-Security-Policy': "default-src 'self'; frame-ancestors 'none';", +}; + +export const HSTS_HEADER = 'max-age=31536000; includeSubDomains; preload'; + +/** + * Attaches configured security headers to an HTTP ServerResponse. + */ +export function applySecurityHeaders( + req: IncomingMessage, + res: ServerResponse, + config: SecurityHeadersConfig = {} +): void { + const isProd = config.isProduction ?? process.env.NODE_ENV === 'production'; + const enableHsts = config.enableHsts ?? isProd; + + for (const [header, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) { + if (!res.hasHeader(header)) { + res.setHeader(header, value); + } + } + + if (enableHsts && !res.hasHeader('Strict-Transport-Security')) { + res.setHeader('Strict-Transport-Security', HSTS_HEADER); + } +}