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

  1. Pass only plain rooted pathnames to request(); never embed a host or scheme.
  2. Audit any code that interpolates external/untrusted data into a request path.
  3. 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

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


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/219e189bbda74eb5. Report an issue: GitHub.