Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions src/main/webui/server/WebUIManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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());

Expand Down
30 changes: 30 additions & 0 deletions src/main/webui/server/__tests__/security-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Request, Response } from 'express';
import { createSecurityMiddleware } from '../security-middleware';

describe('createSecurityMiddleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
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();
});
});
52 changes: 52 additions & 0 deletions src/main/webui/server/security-middleware.ts
Original file line number Diff line number Diff line change
@@ -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 <object>, <embed>, <applet>
"base-uri 'self'", // Restrict <base> tag
];

res.setHeader('Content-Security-Policy', cspDirectives.join('; '));

next();
};
}