fix(cli): validate session ids (#2475)

🎯 **What:** The vulnerability fixed is a Path Traversal issue involving
the `sessionId` string. Previously, the system appended the `sessionId`
to file paths without validating it, allowing attackers to inject
malicious characters (like `../`) and manipulate paths (e.g.
`server.sock` or `daemon.pid`).

⚠️ **Risk:** The potential impact if left unfixed would allow arbitrary
file writes. Because MCP can run with elevated privileges or alongside
sensitive projects, letting users (or clients triggering the daemon)
write files anywhere on the system could lead to complete system
compromise.

🛡️ **Solution:** The fix introduces a new `assertValidSessionId` utility
that enforces strict UUID character validation (`/^[a-fA-F0-9-]+$/`). If
an invalid payload is found, an Error is thrown immediately. This
validation check has been added to CLI options, daemon startup routines,
and deeply within the utility methods like `getSocketPath` and
`getRuntimeHome` to provide a defense-in-depth approach. Unit tests have
been added to verify that valid paths pass and traversal attempts are
successfully blocked.

---
*PR created automatically by Jules for task
[16000164307153409218](https://jules.google.com/task/16000164307153409218)
started by @OrKoN*

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: OrKoN <399150+OrKoN@users.noreply.github.com>
This commit is contained in:
Alex Rudenko
2026-08-05 15:01:06 +02:00
committed by GitHub
parent d302a47716
commit 5ca9121d18
4 changed files with 56 additions and 2 deletions
+9 -1
View File
@@ -20,7 +20,11 @@ import {
verifyDaemonVersion,
} from '../daemon/client.js';
import type {DaemonStatusResult} from '../daemon/types.js';
import {isDaemonRunning, serializeArgs} from '../daemon/utils.js';
import {
isDaemonRunning,
serializeArgs,
assertValidSessionId,
} from '../daemon/utils.js';
import {logDisclaimers} from '../index.js';
import {hideBin, yargs, type CallToolResult} from '../third_party/index.js';
import {checkForUpdates} from '../utils/check-for-updates.js';
@@ -76,6 +80,10 @@ const y = yargs(hideBin(process.argv))
description: 'Session ID for daemon scoping',
default: '',
hidden: true,
coerce: (sessionId: string) => {
assertValidSessionId(sessionId);
return sessionId;
},
})
.demandCommand()
.version(VERSION)
+2
View File
@@ -28,9 +28,11 @@ import {
INDEX_SCRIPT_PATH,
IS_WINDOWS,
isDaemonRunning,
assertValidSessionId,
} from './utils.js';
const sessionId = process.env.CHROME_DEVTOOLS_MCP_SESSION_ID || '';
assertValidSessionId(sessionId);
logger?.(`Daemon sessionId: ${sessionId}`);
if (isDaemonRunning(sessionId)) {
logger?.('Another daemon process is running.');
+14
View File
@@ -23,8 +23,18 @@ export const INDEX_SCRIPT_PATH = path.join(
const APP_NAME = 'chrome-devtools-mcp';
export const DAEMON_CLIENT_NAME = 'chrome-devtools-cli-daemon';
export function assertValidSessionId(sessionId: string): void {
if (!sessionId) {
return;
}
if (!/^[a-fA-F0-9-]+$/.test(sessionId)) {
throw new Error(`Invalid sessionId: ${sessionId}`);
}
}
// Using these paths due to strict limits on the POSIX socket path length.
export function getSocketPath(sessionId: string): string {
assertValidSessionId(sessionId);
const uid = os.userInfo().uid;
const username = os.userInfo().username;
const suffix = sessionId ? `-${sessionId}` : '';
@@ -49,6 +59,7 @@ export function getSocketPath(sessionId: string): string {
}
export function getRuntimeHome(sessionId: string): string {
assertValidSessionId(sessionId);
const platform = os.platform();
const uid = os.userInfo().uid;
const suffix = sessionId ? `-${sessionId}` : '';
@@ -72,11 +83,13 @@ export function getRuntimeHome(sessionId: string): string {
export const IS_WINDOWS = os.platform() === 'win32';
export function getPidFilePath(sessionId: string) {
assertValidSessionId(sessionId);
const runtimeDir = getRuntimeHome(sessionId);
return path.join(runtimeDir, 'daemon.pid');
}
export function getDaemonPid(sessionId: string) {
assertValidSessionId(sessionId);
try {
const pidFile = getPidFilePath(sessionId);
logger?.(`Daemon pid file ${pidFile} sessionId=${sessionId}`);
@@ -96,6 +109,7 @@ export function getDaemonPid(sessionId: string) {
}
export function isDaemonRunning(sessionId: string): boolean {
assertValidSessionId(sessionId);
const pid = getDaemonPid(sessionId);
if (pid) {
try {
+31 -1
View File
@@ -8,9 +8,39 @@ import assert from 'node:assert';
import {describe, it} from 'node:test';
import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
import {serializeArgs} from '../../src/daemon/utils.js';
import {serializeArgs, assertValidSessionId} from '../../src/daemon/utils.js';
import type {YargsOptions} from '../../src/third_party/index.js';
describe('assertValidSessionId', () => {
it('should not throw for empty sessionId', () => {
assert.doesNotThrow(() => assertValidSessionId(''));
});
it('should not throw for valid UUID', () => {
assert.doesNotThrow(() =>
assertValidSessionId('123e4567-e89b-12d3-a456-426614174000'),
);
assert.doesNotThrow(() =>
assertValidSessionId('aabbccdd-1122-3344-5566-77889900aabb'),
);
});
it('should throw for invalid sessionId formats', () => {
assert.throws(
() => assertValidSessionId('../../../etc/passwd'),
/Invalid sessionId/,
);
assert.throws(
() => assertValidSessionId('sessionId_with_underscore'),
/Invalid sessionId/,
);
assert.throws(
() => assertValidSessionId('session@id'),
/Invalid sessionId/,
);
});
});
describe('serializeArgs', () => {
it('should ignore undefined or null values', () => {
const options: Record<string, YargsOptions> = {