{"record":{"id":"6f70fd19683ffe60","repo":"thedotmack/claude-mem","slug":"admin-endpoints-are-only-accessible-from-localhost","errorCode":null,"errorMessage":"Admin endpoints are only accessible from localhost","messagePattern":"Admin endpoints are only accessible from localhost","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"src/services/worker/http/middleware.ts","lineNumber":88,"sourceCode":"    }\n    next();\n  };\n}\n\nexport function requireLocalhost(req: Request, res: Response, next: NextFunction): void {\n  const clientIp = req.ip || req.connection.remoteAddress || '';\n  const isLocalhost =\n    clientIp === '127.0.0.1' ||\n    clientIp === '::1' ||\n    clientIp === '::ffff:127.0.0.1' ||\n    clientIp === 'localhost';\n\n  if (!isLocalhost) {\n    logger.warn('SECURITY', 'Admin endpoint access denied - not localhost', {\n      endpoint: req.path,\n      clientIp,\n      method: req.method\n    });\n    res.status(403).json({\n      error: 'Forbidden',\n      message: 'Admin endpoints are only accessible from localhost'\n    });\n    return;\n  }\n\n  next();\n}\n\n// ---------------------------------------------------------------------------\n// Observation TV remote read-only broadcast guard.\n//\n// The worker's HTTP surface has no request authentication; its only defence is\n// the loopback bind. When the operator opens the bind (CLAUDE_MEM_WORKER_HOST)\n// so a phone or a spare monitor can watch Observation TV, this guard is the\n// whole security boundary: loopback requests are untouched, and every\n// non-loopback request is default-denied except an exact-match allowlist of","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/worker/http/middleware.ts#L70-L106","documentation":"This is the JSON 403 response body message sent by the requireLocalhost middleware when an admin endpoint is requested from a non-localhost client IP. It is a deliberate security guard: admin routes must only be reachable from the local machine, and the access-denied event is logged under the SECURITY logger with endpoint, IP, and method.","triggerScenarios":"Any HTTP request to an admin endpoint whose resolved clientIp is not a localhost address (127.0.0.1/::1), triggering the isLocalhost check to fail.","commonSituations":"Accessing the worker admin API from another machine on the network; requests forwarded through a proxy/load balancer so the apparent client IP is remote; misconfigured bind/port exposing the worker externally.","solutions":["Run the requesting client on the same host and target 127.0.0.1 (or ::1) instead of a LAN/public IP.","If remote access is required, put an authenticated local proxy/tunnel (e.g. SSH port forward) in front of the endpoint rather than exposing it.","Check clientIp in the log to see why the request appears non-local (proxy X-Forwarded-For handling).","Ensure the worker binds only to the loopback interface."],"exampleFix":"// before\ncurl http://192.168.1.10:3838/admin/stats\n// after\ncurl http://127.0.0.1:3838/admin/stats","handlingStrategy":"validation","validationCode":"const url = new URL(adminUrl);\nif (!['127.0.0.1','localhost','[::1]'].includes(url.hostname)) {\n  throw new Error('Admin endpoints must be reached via localhost');\n}","typeGuard":"const isLocalhostUrl = (u: string) =>\n  ['127.0.0.1','localhost','::1'].includes(new URL(u).hostname);","tryCatchPattern":"const res = await fetch(adminUrl);\nif (res.status === 403) {\n  // non-localhost access blocked; switch to 127.0.0.1 or set up an SSH tunnel\n}","preventionTips":["Always target 127.0.0.1 for admin routes","Never expose the worker port beyond loopback","Use SSH tunnels or an authenticated proxy for remote admin access"],"tags":["security","localhost","admin-endpoint"],"backgroundTag":"permission-denied","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}