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