Skip to content

[Security] brokerCommand config-driven — write-to-config = code exec #20

@iRonin

Description

@iRonin

Summary

brokerCommand and brokerArgs are read from ~/.pi/agent/intercom/config.json and passed straight to child_process.spawn. Any process with write access to that file can replace the command, and the next pi session that auto-spawns the broker will execute the attacker's binary with the user's privileges, in the extension directory, with process.env inherited. There is no allowlist, no integrity check, and no warning.

Affected code

  • File: config.ts:51-72 (verified at commit 5caa4aa)
if (Object.hasOwn(parsedConfig, "brokerCommand")) {
  if (typeof parsedConfig.brokerCommand !== "string") {
    throw new Error(`"brokerCommand" must be a string`);
  }
  const brokerCommand = parsedConfig.brokerCommand.trim();
  if (!brokerCommand) {
    throw new Error(`"brokerCommand" must not be empty`);
  }
  config.brokerCommand = brokerCommand;
}

if (Object.hasOwn(parsedConfig, "brokerArgs")) {
  if (!Array.isArray(parsedConfig.brokerArgs)) {
    throw new Error(`"brokerArgs" must be an array`);
  }
  // ... only typechecks string elements ...
  config.brokerArgs = brokerArgs;
}
  • File: broker/spawn.ts:84-107 (getBrokerLaunchSpec) and broker/spawn.ts:128-180 (spawnBrokerIfNeeded)
return {
  kind: "direct",
  command: brokerCommand,
  args: [...brokerArgs, brokerPath],
};
// ...
const child = spawn(launch.command, launch.args, getBrokerSpawnOptions());

The getBrokerSpawnOptions env explicitly forwards ...process.env, so the spawned process inherits every secret the user's shell has — OPENAI_API_KEY, ANTHROPIC_API_KEY, KILOCODE_TOKEN, AWS creds, etc. — plus the extension directory as cwd.

Reproduction / attack scenario

  1. Attacker writes once to ~/.pi/agent/intercom/config.json:
    {
      "brokerCommand": "/tmp/.attacker/payload.sh",
      "brokerArgs": []
    }
  2. Next time the user starts a pi session (or any existing session triggers a reconnect after the broker idle-shutdown 5s timer fires), spawnBrokerIfNeeded sees no live broker, calls getBrokerLaunchSpec with the attacker's command, and runs /tmp/.attacker/payload.sh <pathToBrokerScript> with the user's full env. The attacker now has code execution as the user with all of pi's API keys.
  3. The child.unref() and detached: true mean the malicious process keeps running after the pi session exits. There is no log telling the user this happened, beyond a possibly-orphan broker socket.

This is meaningfully different from "attacker has filesystem write" being already-game-over: many attack surfaces let an attacker write a single bounded-scope file (a misconfigured editor extension, a vulnerable file-upload tool, a sloppy install script touching ~/.pi) without giving them general code execution. This config-file-to-RCE primitive bridges those.

Impact

HIGH. Write to one specific JSON file → arbitrary code execution on next pi launch with the user's secrets in env. The window is permanent: once written, the malicious config persists across reboots and pi upgrades. The user has no UI signal that the broker command has changed.

Suggested mitigation

The cleanest fix is to drop the configurability: have pi-intercom always launch its own broker via the bundled tsx CLI (getTsxCliPath(extensionDir)), with the broker script path resolved relative to the extension directory. The README mentions a bun/npx alternative — that workflow is rare enough to either drop or move behind a separate, off-by-default flag with a documented threat model.

If configurability has to stay:

  1. Allowlist brokerCommand to a small set: "npx", "bun", "node", full-path to process.execPath. Anything else → reject and warn.
  2. Refuse to load a config.json whose owner or perms allow writes by anyone other than the user (fs.statSync(CONFIG_PATH).mode & 0o022 should be zero, owner uid should match process.getuid()). On Linux/macOS this is chmod 0600 + ownership check; the same logic should apply to ~/.pi/agent/intercom/ itself.
  3. Resolve brokerCommand to an absolute path and stat it. If the resolved binary is world-writable or sits in a world-writable directory, refuse. (Not a complete defence, but raises the bar.)
  4. Show a one-time prompt the first time brokerCommand differs from the default, the way most editor extensions confirm "trust this workspace's binary path" prompts. Persist the user's decision in a separate trust file outside intercom/.

Mitigation precedent: VS Code's deno/PHP/python extensions all hit this exact bug (config.json-points-to-binary → workspace RCE) and ended up with a per-binary trust-on-first-use prompt plus an absolute-path-resolution check.

Environment

  • Repo: nicobailon/pi-intercom @ 5caa4aa
  • Reported by: external security review (read-only audit)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions