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
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).
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.
Example fix
// before await request(port, 'GET', '//evil.example.com/api/sessions'); // after await request(port, 'GET', '/api/sessions');
Defensive patterns
Strategy: validation
Validate before calling
function assertLoopbackPath(p) {
const url = new URL(p, 'http://127.0.0.1');
if (url.hostname !== '127.0.0.1') throw new Error(`path escapes loopback server: ${p}`);
return url.pathname + url.search;
} Type guard
function staysOnLoopback(v) { try { return new URL(v, 'http://127.0.0.1').hostname === '127.0.0.1'; } catch { return false; } } Try / catch
try {
const res = await request(port, method, requestPath);
} catch (err) {
if (err.message.includes('must stay on the loopback server')) {
console.error('Strip any host from the path; only root-relative paths are allowed.');
process.exit(2);
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Concatenating user- or config-supplied strings into request paths; protocol-relative URLs sneaking in from template strings; security probing of the local server surface.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- remote import host resolves to a non-public address
- artifact must be a resident regular file
- artifact path escapes output directory
- artifact path must be canonical and absolute
- artifact permits provider execution
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/219e189bbda74eb5.
Report an issue: GitHub.
Appendix: 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 8321021c54)