Skip to content

security: enterprise review fixes — HTTPS enforcement, input limits, path redaction, CI alignment#49

Open
ashish993 wants to merge 4 commits into
CALLE-AI:mainfrom
ashish993:main
Open

security: enterprise review fixes — HTTPS enforcement, input limits, path redaction, CI alignment#49
ashish993 wants to merge 4 commits into
CALLE-AI:mainfrom
ashish993:main

Conversation

@ashish993

Copy link
Copy Markdown

Summary

Enterprise security review fixes across packages/cli, packages/core, and .github/workflows. All changes pass pnpm check and pnpm test (27 unit + 13 E2E tests).

Changes

Critical

  • C-2: Remove cache_path / pending_cache_path (full home-directory paths) from all public JSON outputs (publicPendingLoginPayload, publicLoginPayload, statusPayload). statusPayload now also validates pending_login_url is HTTPS before including it.
  • C-3: Validate login_url from broker is HTTPS before writing to stderr or passing to openBrowser.

High

  • H-1: Add 64 KB max-size guard on --args-json in parseJsonObject() to prevent oversized payloads.
  • H-2: Cap --to-phone at 10 numbers and --goal at 2000 characters in buildPlanArguments().
  • H-4: Enforce HTTPS on all user-supplied base URLs (--base-url, --server-url, --broker-base-url, --auth-base-url). Loopback addresses (localhost, 127.0.0.1) are allowed with HTTP for local development/testing.

Medium

  • M-5: Align CI workflow to Node 24 (was 22) to match the release workflow.

Low

  • L-1: Truncate server-controlled error messages to 200 chars and strip newlines in McpHttpError to prevent server-injected content from reaching LLM context.
  • L-2: CALLE_TELEMETRY_URL misconfiguration no longer throws and blocks all CLI commands — warns to stderr and disables telemetry gracefully.
  • L-4: loginCommand() omits --cache-root from the hint string when it equals the default (~/.calle-mcp/cli), avoiding home-directory path leakage in assistant hints.

Testing

pnpm check   # all syntax/lint checks pass
pnpm test    # 27 unit + 13 E2E tests pass

ashish993 added 4 commits May 24, 2026 14:44
- Pin all GitHub Actions to immutable commit SHAs to prevent supply
  chain attacks via mutable tag references (actions/checkout,
  pnpm/action-setup, actions/setup-node, changesets/action)
- Validate login_url is HTTPS before passing to system browser opener
  to prevent file:// or other protocol exploitation from a compromised
  broker response
- Sanitize login_url in preAuthHelpMessage to HTTPS-only before
  embedding in LLM agent skill prompts to prevent prompt injection
  from a compromised broker
- Replace MD5 with SHA-256 in serverHash() cache key (packages/core/lib/cache.js)
- Validate CALLE_TELEMETRY_URL is HTTPS before use (packages/cli/lib/config.js)
- Add max polling attempt cap (600) to loginWithBroker alongside time deadline (packages/core/lib/broker-client.js)
- Add retry with exponential backoff for 429/502/503/504 in requestJsonRpc (packages/core/lib/mcp-client.js)
- Add MCP protocol version mismatch warning to stderr on initialize (packages/core/lib/mcp-client.js + cli.js)
- C-2: remove cache_path/pending_cache_path (home dir paths) from all
  public JSON payloads in cli.js; statusPayload now validates
  pending_login_url HTTPS before including it
- H-1: add 64 KB max-size guard on --args-json in parseJsonObject()
- H-2: add max 10 --to-phone numbers and max 2000 char --goal limit
  in buildPlanArguments()
- L-1: truncate server-controlled error messages to 200 chars and
  strip newlines in McpHttpError to prevent injection into LLM context
- L-4: omit --cache-root from loginCommand() output when it equals
  the default (~/.calle-mcp/cli) to avoid leaking home paths in hints
- M-5: align CI workflow to Node 24 (matches release workflow)
- Also: allow http:// on loopback (localhost/127.0.0.1) for local
  dev/test; HTTPS enforcement still applies for all external URLs
Copilot AI review requested due to automatic review settings May 24, 2026 07:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Security-hardening changes across the CLI and core libraries to address enterprise review findings, primarily focusing on preventing sensitive path leakage, enforcing HTTPS for externally supplied URLs, and bounding user/server-controlled inputs to reduce injection and resource-exhaustion risk.

Changes:

  • Enforce HTTPS (with loopback-only HTTP exception) for CLI-supplied base/server/broker/auth URLs; validate broker-provided login URLs before printing/opening.
  • Add input/output hardening: caps for --args-json, --to-phone, --goal; truncate/strip newlines from server-controlled MCP error messages; redact cache paths from public JSON payloads.
  • Align CI to Node 24 and pin GitHub Actions to specific SHAs.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/core/lib/mcp-client.js Adds retry logic, protocol mismatch warning hook, and sanitizes remote error messages.
packages/core/lib/cache.js Changes cache key hashing from MD5 to SHA-256.
packages/core/lib/broker-client.js Enforces HTTPS for broker login URL before printing/opening; adds polling attempt cap.
packages/cli/lib/config.js Adds HTTPS enforcement for user-supplied endpoint URLs; makes telemetry URL misconfig non-fatal.
packages/cli/lib/cli.js Adds input size/length limits, redacts cache paths from public outputs, and tightens login URL handling.
.gitignore Ignores local run.md notes file.
.github/workflows/release.yml Pins actions to SHAs; uses Node 24.
.github/workflows/ci.yml Pins actions to SHAs; updates CI Node version to 24.
Comments suppressed due to low confidence (1)

packages/cli/lib/cli.js:406

  • publicPendingLoginPayload still returns the broker-provided login_url verbatim in JSON output. Since login_url is server-controlled, this can re-introduce unsafe/non-HTTPS URLs into public outputs even though other paths now validate/redact. Consider only including login_url when it parses as https: (or omitting/setting null otherwise), similar to statusPayload and the stderr/openBrowser safeguards.
    pending_status: pending.status,
    pending_created: created,
    login_url: pending.login_url,
    ...(assistantHint ? { assistant_hint: assistantHint } : {}),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +69
async function requestJsonRpc(fetchImpl, url, { headers, payload, timeoutMs, sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
let lastError;
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
headers: responseHeaders,
});
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRY_ATTEMPTS) {
lastError = err;
Comment on lines +108 to +109
const safeMessage = rawMessage
? rawMessage.slice(0, 200).replace(/[\r\n]+/g, " ").trim()
const retryAfter = Number(retryAfterHeader);
if (retryAfterHeader && !Number.isNaN(retryAfter) && retryAfter > 0) {
return Math.min(retryAfter * 1000, 30000);
}
Comment on lines 152 to 167
export function resolveRuntimeConfig(options = {}, env = process.env) {
if (options.baseUrl) {
requireHttpsUrl(options.baseUrl, "--base-url");
}
if (options.serverUrl) {
requireHttpsUrl(options.serverUrl, "--server-url");
}
if (options.brokerBaseUrl) {
requireHttpsUrl(options.brokerBaseUrl, "--broker-base-url");
}
if (options.authBaseUrl) {
requireHttpsUrl(options.authBaseUrl, "--auth-base-url");
}
const baseUrl = normalizeBaseUrl(options.baseUrl || DEFAULT_BASE_URL);
const channel = options.channel || DEFAULT_CHANNEL;
const serverUrl = resolveServerUrl({ serverUrl: options.serverUrl, baseUrl, channel });
Comment thread packages/cli/lib/cli.js
Comment on lines +353 to +356
const rawStr = String(raw);
if (Buffer.byteLength(rawStr, "utf8") > ARGS_JSON_MAX_BYTES) {
throw new InvalidArgumentsError(`${optionName} exceeds maximum size of 64 KB`);
}
Comment thread packages/cli/lib/cli.js
Comment on lines +651 to +653
if (toPhones.length > 10) {
throw new InvalidArgumentsError("--to-phone: maximum 10 numbers per request");
}
Comment thread packages/cli/lib/cli.js
Comment on lines +655 to +658
const goal = requireStringOption(options, "goal", "--goal");
if (goal.length > 2000) {
throw new InvalidArgumentsError("--goal: maximum 2000 characters");
}
Comment on lines 5 to 7
export function serverHash(serverUrl) {
return crypto.createHash("md5").update(serverUrl, "utf8").digest("hex");
return crypto.createHash("sha256").update(serverUrl, "utf8").digest("hex");
}

@Ray-56 Ray-56 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed locally. I recommend holding this PR until the following issues are addressed.

  1. requestJsonRpc retries non-idempotent tools/call requests.

packages/core/lib/mcp-client.js now retries every JSON-RPC request on 429/502/503/504 and some thrown fetch errors. That includes tools/call, which can execute non-idempotent operations such as run_call. If the server processes the first request but returns a transient 5xx, or if the response is lost, the retry can trigger duplicate side effects such as duplicate phone calls.

I reproduced this locally with a fake MCP server: the first tools/call returned 503, the client retried, and the server saw toolCalls: 2. Please restrict retries to safe/idempotent MCP methods such as initialize / tools/list, or add an explicit idempotency mechanism before retrying tool calls.

  1. Broker-controlled login_url is still returned verbatim in public JSON outputs.

The PR sanitizes the URL used in assistant_hint, stderr, and browser opening, but publicPendingLoginPayload() and authRequiredPayload() can still include the raw pending.login_url / cached pending login_url as top-level JSON fields. I reproduced auth login --start-only returning:

"login_url": "file:///tmp/injected"

That reintroduces the unsafe server-controlled URL into the public surface this PR is trying to harden. Please reuse the same HTTPS-only sanitizer for any public login_url field, or omit the field when the URL is not safe.

Validation run on the PR head in a detached worktree with Node v22.21.1:

  • pnpm test passed
  • pnpm check passed
  • pnpm pack:dry-run passed

Release note: because this changes published behavior in @call-e/cli and @call-e/core, a patch changeset for both packages is recommended before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants