{"record":{"id":"219e189bbda74eb5","repo":"affaan-m/ECC","slug":"plan-canvas-request-path-must-stay-on-the-loopback-server","errorCode":null,"errorMessage":"plan-canvas request path must stay on the loopback server","messagePattern":"plan-canvas request path must stay on the loopback server","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/plan-canvas.js","lineNumber":107,"sourceCode":"    return null;\n  }\n}\n\nfunction validatePort(port) {\n  const value = Number(port);\n  if (!Number.isInteger(value) || value < 0 || value > 65535) {\n    throw new Error(`invalid plan-canvas server port: ${port}`);\n  }\n  return value;\n}\n\nfunction validateRequestPath(requestPath) {\n  if (typeof requestPath !== 'string' || !requestPath.startsWith('/')) {\n    throw new Error('plan-canvas request path must be root-relative');\n  }\n  const url = new URL(requestPath, `http://${DEFAULT_HOST}`);\n  if (url.hostname !== DEFAULT_HOST) {\n    throw new Error('plan-canvas request path must stay on the loopback server');\n  }\n  if (!SAFE_REQUEST_PATHS.has(url.pathname) && !SESSION_REPLY_PATH.test(url.pathname)) {\n    throw new Error(`unsupported plan-canvas request path: ${url.pathname}`);\n  }\n  return `${url.pathname}${url.search}`;\n}\n\nfunction requestOptions(port, method, requestPath, headers) {\n  return {\n    host: DEFAULT_HOST,\n    port: validatePort(port),\n    method,\n    path: validateRequestPath(requestPath),\n    agent: false,\n    headers\n  };\n}\n","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/plan-canvas.js#L89-L125","documentation":"After parsing the request path as a URL against the loopback base, validateRequestPath confirms the resolved hostname equals DEFAULT_HOST. A path like '//evil.example.com/x' resolves to a different host, so the guard rejects any attempt to redirect the request off the local plan-canvas server (SSRF-style protection).","triggerScenarios":"Passing a path beginning with '//' or containing a full absolute URL ('http://otherhost/api/sessions') to the request helper — `new URL(requestPath, 'http://127.0.0.1')` then yields a foreign hostname.","commonSituations":"Concatenating user- or config-supplied strings into request paths; protocol-relative URLs sneaking in from template strings; security probing of the local server surface.","solutions":["Use a root-relative path only, e.g. '/api/sessions'.","Never embed a host in the path argument; the host/port are fixed by requestOptions.","Sanitize any externally sourced segment before interpolation (strip leading '//').","If a different server is genuinely needed, change the server config, not the request path."],"exampleFix":"// before\nawait request(port, 'GET', '//evil.example.com/api/sessions');\n// after\nawait request(port, 'GET', '/api/sessions');","handlingStrategy":"validation","validationCode":"function assertLoopbackPath(p) {\n  const url = new URL(p, 'http://127.0.0.1');\n  if (url.hostname !== '127.0.0.1') throw new Error(`path escapes loopback server: ${p}`);\n  return url.pathname + url.search;\n}","typeGuard":"function staysOnLoopback(v) { try { return new URL(v, 'http://127.0.0.1').hostname === '127.0.0.1'; } catch { return false; } }","tryCatchPattern":"try {\n  const res = await request(port, method, requestPath);\n} catch (err) {\n  if (err.message.includes('must stay on the loopback server')) {\n    console.error('Strip any host from the path; only root-relative paths are allowed.');\n    process.exit(2);\n  }\n  throw err;\n}","preventionTips":["Treat request paths as data, never interpolate hostnames or full URLs into them","Sanitize any externally supplied segment (strip leading '//')","Review template-literal path construction for protocol-relative URL injection","Keep this SSRF guard; do not loosen it for convenience"],"tags":["security","http","ssrf"],"backgroundTag":"invalid-url","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}