From 0cb3256b2d07d8521912f8ef77c27735b58dca9b Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Wed, 17 Dec 2025 19:06:33 -0500 Subject: [PATCH] fix(security): add localhost-only protection for admin endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds middleware to restrict /api/admin/restart and /api/admin/shutdown to localhost-only access. This prevents DoS attacks when the worker service is bound to 0.0.0.0 for remote UI access. Implementation: - Created requireLocalhost middleware in middleware.ts - Applied to both admin endpoints - Checks client IP against localhost addresses (127.0.0.1, ::1, etc.) - Returns 403 Forbidden for non-localhost requests Addresses security concern raised in PR #368 with cleaner DRY approach. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/services/worker-service.ts | 8 ++++---- src/services/worker/http/middleware.ts | 28 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/services/worker-service.ts b/src/services/worker-service.ts index 86f0d5c6..4b7837fe 100644 --- a/src/services/worker-service.ts +++ b/src/services/worker-service.ts @@ -32,7 +32,7 @@ import { TimelineService } from './worker/TimelineService.js'; import { SessionEventBroadcaster } from './worker/events/SessionEventBroadcaster.js'; // Import HTTP layer -import { createMiddleware, summarizeRequestBody as summarizeBody } from './worker/http/middleware.js'; +import { createMiddleware, summarizeRequestBody as summarizeBody, requireLocalhost } from './worker/http/middleware.js'; import { ViewerRoutes } from './worker/http/routes/ViewerRoutes.js'; import { SessionRoutes } from './worker/http/routes/SessionRoutes.js'; import { DataRoutes } from './worker/http/routes/DataRoutes.js'; @@ -208,8 +208,8 @@ export class WorkerService { } }); - // Admin endpoints for process management - this.app.post('/api/admin/restart', async (_req, res) => { + // Admin endpoints for process management (localhost-only) + this.app.post('/api/admin/restart', requireLocalhost, async (_req, res) => { res.json({ status: 'restarting' }); // On Windows, if managed by wrapper, send message to parent to handle restart @@ -230,7 +230,7 @@ export class WorkerService { } }); - this.app.post('/api/admin/shutdown', async (_req, res) => { + this.app.post('/api/admin/shutdown', requireLocalhost, async (_req, res) => { res.json({ status: 'shutting_down' }); // On Windows, if managed by wrapper, send message to parent to handle shutdown diff --git a/src/services/worker/http/middleware.ts b/src/services/worker/http/middleware.ts index 52fcd19d..b1359df4 100644 --- a/src/services/worker/http/middleware.ts +++ b/src/services/worker/http/middleware.ts @@ -60,6 +60,34 @@ export function createMiddleware( return middlewares; } +/** + * Middleware to require localhost-only access + * Used for admin endpoints that should not be exposed when binding to 0.0.0.0 + */ +export function requireLocalhost(req: Request, res: Response, next: NextFunction): void { + const clientIp = req.ip || req.connection.remoteAddress || ''; + const isLocalhost = + clientIp === '127.0.0.1' || + clientIp === '::1' || + clientIp === '::ffff:127.0.0.1' || + clientIp === 'localhost'; + + if (!isLocalhost) { + logger.warn('SECURITY', 'Admin endpoint access denied - not localhost', { + endpoint: req.path, + clientIp, + method: req.method + }); + res.status(403).json({ + error: 'Forbidden', + message: 'Admin endpoints are only accessible from localhost' + }); + return; + } + + next(); +} + /** * Summarize request body for logging * Used to avoid logging sensitive data or large payloads