fix(security): add localhost-only protection for admin endpoints

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 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-17 19:06:33 -05:00
parent 44029862b1
commit 0cb3256b2d
2 changed files with 32 additions and 4 deletions
+4 -4
View File
@@ -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
+28
View File
@@ -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