affaan-m/ECC · error · Error
plan-canvas request path must stay on the loopback server
Error message
plan-canvas request path must stay on the loopback server
What it means
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.
Source
Thrown at scripts/plan-canvas.js:107
return null;
}
}
function validatePort(port) {
const value = Number(port);
if (!Number.isInteger(value) || value < 0 || value > 65535) {
throw new Error(`invalid plan-canvas server port: ${port}`);
}
return value;
}
function validateRequestPath(requestPath) {
if (typeof requestPath !== 'string' || !requestPath.startsWith('/')) {
throw new Error('plan-canvas request path must be root-relative');
}
const url = new URL(requestPath, `http://${DEFAULT_HOST}`);
if (url.hostname !== DEFAULT_HOST) {
throw new Error('plan-canvas request path must stay on the loopback server');
}
if (!SAFE_REQUEST_PATHS.has(url.pathname) && !SESSION_REPLY_PATH.test(url.pathname)) {
throw new Error(`unsupported plan-canvas request path: ${url.pathname}`);
}
return `${url.pathname}${url.search}`;
}
function requestOptions(port, method, requestPath, headers) {
return {
host: DEFAULT_HOST,
port: validatePort(port),
method,
path: validateRequestPath(requestPath),
agent: false,
headers
};
}
View on GitHub (pinned to 01e15490f0)
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.
Example fix
// before (internal call, hostname smuggled) request(port, 'GET', '//attacker.example/api/sessions'); // after request(port, 'GET', '/api/sessions');
Defensive patterns
Strategy: validation
Validate before calling
function assertLoopbackPath(requestPath, host) {
const url = new URL(requestPath, `http://${host}`);
if (url.hostname !== host) {
throw new Error('Refusing request path that escapes the loopback host');
}
return url;
} Prevention
- 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.
When it happens
Trigger: A path like `//evil.com/api/sessions` or `/ evill.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`.
Common situations: 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.
Related errors
- Missing value for --target
- Unknown catalog command: ${options.command}
- unsupported plan-canvas request path: ${url.pathname}
- Failed to read ${path.basename(filePath)}: ${error.message}
- invalid plan-canvas server port: ${port}
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/219e189bbda74eb5.
Report an issue: GitHub.