From e2905d602e8e3b7b0d91218ef1a79953c026aae1 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:22:04 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20Ad?= =?UTF-8?q?d=20security=20headers=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented `createSecurityMiddleware` to add X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Content-Security-Policy. - Integrated middleware in `WebUIManager.ts`. - Added unit tests for the middleware. - Configured CSP to be compatible with GridStack (inline styles) and video streaming (WebSocket/HLS). --- .jules/sentinel.md | 5 ++ src/main/webui/server/WebUIManager.ts | 4 ++ .../__tests__/security-middleware.test.ts | 30 +++++++++++ src/main/webui/server/security-middleware.ts | 52 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 src/main/webui/server/__tests__/security-middleware.test.ts create mode 100644 src/main/webui/server/security-middleware.ts diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 03026c1..8078bfe 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -12,3 +12,8 @@ **Vulnerability:** The `JobStartRequestSchema` validated filenames only by length (`min(1)`), allowing path traversal characters (e.g., `../../etc/passwd`). If the backend naively concatenates this filename to a path, it allows arbitrary file read/write. **Learning:** Zod's string validation is basic. For file paths, explicit validation against directory traversal (e.g., forbidding `..`) is essential, especially when inputs are passed to filesystem operations. **Prevention:** Use `.refine()` in Zod schemas to reject strings containing `..` path segments: `/(^|[\/])\.\.([\/]|$)/`. + +## 2026-01-24 - Missing Security Headers +**Vulnerability:** The WebUI server was missing standard HTTP security headers (X-Frame-Options, X-Content-Type-Options, Content-Security-Policy, etc.), potentially exposing it to clickjacking, MIME-sniffing, and XSS attacks. +**Learning:** Express.js does not include security headers by default. Explicit middleware is required to set them. +**Prevention:** Always include security headers middleware in the Express app setup pipeline to set X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Content-Security-Policy headers. diff --git a/src/main/webui/server/WebUIManager.ts b/src/main/webui/server/WebUIManager.ts index 44db9c2..d5590a1 100644 --- a/src/main/webui/server/WebUIManager.ts +++ b/src/main/webui/server/WebUIManager.ts @@ -43,6 +43,7 @@ import { createLoginRateLimiter, createRequestLogger, } from './auth-middleware.js'; +import { createSecurityMiddleware } from './security-middleware.js'; import { registerPublicThemeRoutes } from './routes/theme-routes.js'; import { getWebSocketManager } from './WebSocketManager.js'; @@ -156,6 +157,9 @@ export class WebUIManager extends EventEmitter { // Request logging this.expressApp.use(createRequestLogger()); + // Security Headers + this.expressApp.use(createSecurityMiddleware()); + // JSON body parsing this.expressApp.use(express.json()); diff --git a/src/main/webui/server/__tests__/security-middleware.test.ts b/src/main/webui/server/__tests__/security-middleware.test.ts new file mode 100644 index 0000000..f0b206b --- /dev/null +++ b/src/main/webui/server/__tests__/security-middleware.test.ts @@ -0,0 +1,30 @@ +import { Request, Response } from 'express'; +import { createSecurityMiddleware } from '../security-middleware'; + +describe('createSecurityMiddleware', () => { + let mockRequest: Partial; + let mockResponse: Partial; + let nextFunction: jest.Mock; + + beforeEach(() => { + mockRequest = {}; + mockResponse = { + setHeader: jest.fn(), + }; + nextFunction = jest.fn(); + }); + + it('should set security headers', () => { + const middleware = createSecurityMiddleware(); + middleware(mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.setHeader).toHaveBeenCalledWith('X-Content-Type-Options', 'nosniff'); + expect(mockResponse.setHeader).toHaveBeenCalledWith('X-Frame-Options', 'SAMEORIGIN'); + expect(mockResponse.setHeader).toHaveBeenCalledWith('Referrer-Policy', 'strict-origin-when-cross-origin'); + expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Security-Policy', expect.stringContaining("default-src 'self'")); + expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Security-Policy', expect.stringContaining("object-src 'none'")); + expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Security-Policy', expect.stringContaining("base-uri 'self'")); + + expect(nextFunction).toHaveBeenCalled(); + }); +}); diff --git a/src/main/webui/server/security-middleware.ts b/src/main/webui/server/security-middleware.ts new file mode 100644 index 0000000..68da458 --- /dev/null +++ b/src/main/webui/server/security-middleware.ts @@ -0,0 +1,52 @@ +/** + * @fileoverview Middleware for adding security headers to HTTP responses. + * + * Implements a defense-in-depth strategy by adding HTTP headers that protect against + * common web vulnerabilities: + * - X-Content-Type-Options: Prevents MIME-sniffing attacks. + * - X-Frame-Options: Protects against clickjacking by restricting iframe embedding. + * - Referrer-Policy: Controls how much referrer information is included with requests. + * - Content-Security-Policy: Restricts sources of executable scripts, styles, and other resources. + * + * The Content-Security-Policy is configured to be compatible with the application's + * use of inline scripts/styles (GridStack, etc.) and WebSocket connections for video streaming, + * while still providing significant protection against object injection and base URI hijacking. + */ + +import { NextFunction, Request, Response } from 'express'; + +/** + * Creates a middleware that adds security headers to every response. + */ +export function createSecurityMiddleware() { + return (_req: Request, res: Response, next: NextFunction): void => { + // Prevent MIME-sniffing + res.setHeader('X-Content-Type-Options', 'nosniff'); + + // Protect against clickjacking (allow same origin) + res.setHeader('X-Frame-Options', 'SAMEORIGIN'); + + // Control referrer information + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + + // Content Security Policy + // We allow 'unsafe-inline' for scripts and styles because the current architecture + // uses inline scripts/styles (e.g., GridStack, video player). + // We allow ws: and wss: for WebSocket connections. + // We allow blob: and data: for media (HLS, MJPEG). + const cspDirectives = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data:", + "connect-src 'self' ws: wss: http: https:", // http/https needed for HLS playlist construction if applicable + "media-src 'self' blob: data:", + "object-src 'none'", // Block , , + "base-uri 'self'", // Restrict tag + ]; + + res.setHeader('Content-Security-Policy', cspDirectives.join('; ')); + + next(); + }; +}