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:
@@ -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)
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
Reference in New Issue
Block a user