{"record":{"id":"219e189bbda74eb5","repo":"affaan-m/ECC","slug":"plan-canvas-request-path-must-stay-on-the-loopback","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":"exception","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/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/plan-canvas.js#L89-L125","documentation":"Thrown by validateRequestPath in scripts/plan-canvas.js when parsing the request path as a URL against the loopback host yields a different hostname. This is an SSRF guard: it prevents a crafted path (e.g. one containing an authority component or a host-switching trick) from directing the CLI's HTTP request away from the local canvas server.","triggerScenarios":"A path like `//evil.com/api/sessions` or `/\tevill.com/...` that, when parsed with new URL(path, 'http://127.0.0.1'), resolves the hostname to something other than DEFAULT_HOST. The check is `url.hostname !== DEFAULT_HOST`.","commonSituations":"Almost never seen in normal use — end users do not pass request paths. This guard exists to harden the internal request layer against maliciously crafted inputs; if it fires, suspect a bug or a tampered path source.","solutions":["Pass only plain rooted pathnames to request(); never embed a host or scheme.","Audit any code that interpolates external/untrusted data into a request path.","If reached via normal CLI usage, file a bug — the CLI's own paths should always satisfy this guard."],"exampleFix":"// before (internal call, hostname smuggled)\nrequest(port, 'GET', '//attacker.example/api/sessions');\n// after\nrequest(port, 'GET', '/api/sessions');","handlingStrategy":"validation","validationCode":"function assertLoopbackPath(requestPath, host) {\n  const url = new URL(requestPath, `http://${host}`);\n  if (url.hostname !== host) {\n    throw new Error('Refusing request path that escapes the loopback host');\n  }\n  return url;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never interpolate external/untrusted input into a request path.","Treat this guard as load-bearing security infrastructure; do not bypass it in tests.","Audit any code change that touches request-path construction."],"tags":["plan-canvas","network","security","ssrf","validation"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}