fix: reject empty, extra, and unknown parts in geolocation and viewport transforms (#3097) (#3120)

Fixes #3097

Closes https://github.com/ChromeDevTools/chrome-devtools-mcp/pull/3124

### Summary
- Enforce strict numeric parsing and tag validation in
`geolocationTransform` and `viewportTransform`
(`src/tools/ToolDefinition.ts`), rejecting empty segments, extra parts,
unknown boolean flags, and non-decimal literals instead of silently
coercing to `0` or dropping them.
- Add regression tests in `tests/tools/emulation.test.ts`.
This commit is contained in:
Yuri (IURII) Trukhin authored and GitHub committed 2026-10-16 07:30:52 +00:00
1 parent 78c8320c50
commit 3ea31fbab1
2 files changed
+78 -11

No files matched your search

+45 -11
View File
@@ -593,6 +593,18 @@ export const timeoutSchema = {
),
};
const DECIMAL = /^[+-]?(\d+(\.\d*)?|\.\d+)$/;
const VALID_VIEWPORT_TAGS = new Set(['mobile', 'touch', 'landscape']);
function parseDecimal(part: string | undefined, name: string): number {
if (part === undefined || !DECIMAL.test(part)) {
throw new Error(
`Invalid ${name} "${part ?? ''}". Expected a decimal number.`,
);
}
return Number(part);
}
export function viewportTransform(arg: string | undefined):
| {
width: number;
@@ -607,33 +619,48 @@ export function viewportTransform(arg: string | undefined):
return undefined;
}
const [dimensions, ...tags] = arg.split(',');
for (const tag of tags) {
if (!VALID_VIEWPORT_TAGS.has(tag)) {
throw new Error(
`Invalid viewport tag "${tag}". Expected format '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'.`,
);
}
}
const isMobile = tags.includes('mobile');
const hasTouch = tags.includes('touch');
const isLandscape = tags.includes('landscape');
const [width, height, dpr] = dimensions.split('x').map(Number) as [
number,
number,
number | undefined,
];
const dimParts = dimensions.split('x');
if (dimParts.length !== 2 && dimParts.length !== 3) {
throw new Error(
`Invalid viewport dimensions "${dimensions}". Expected format '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'.`,
);
}
const width = parseDecimal(dimParts[0], 'viewport width');
if (!Number.isFinite(width) || width <= 0) {
throw new Error(
`Invalid viewport width "${width}". Expected format '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]' with a positive width.`,
);
}
const height = parseDecimal(dimParts[1], 'viewport height');
if (!Number.isFinite(height) || height <= 0) {
throw new Error(
`Invalid viewport height "${height}". Expected format '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]' with a positive height.`,
);
}
if (dpr !== undefined && (!Number.isFinite(dpr) || dpr <= 0)) {
throw new Error(
`Invalid devicePixelRatio "${dpr}". Expected a positive number.`,
);
let deviceScaleFactor: number | undefined;
if (dimParts.length === 3) {
const dpr = parseDecimal(dimParts[2], 'devicePixelRatio');
if (!Number.isFinite(dpr) || dpr <= 0) {
throw new Error(
`Invalid devicePixelRatio "${dpr}". Expected a positive number.`,
);
}
deviceScaleFactor = dpr;
}
return {
width,
height,
deviceScaleFactor: dpr,
deviceScaleFactor,
isMobile,
isLandscape,
hasTouch,
@@ -646,12 +673,19 @@ export function geolocationTransform(
if (!arg) {
return undefined;
}
const [latitude, longitude] = arg.split(',').map(Number) as [number, number];
const parts = arg.split(',');
if (parts.length !== 2) {
throw new Error(
`Invalid geolocation "${arg}". Expected '<latitude>,<longitude>'.`,
);
}
const latitude = parseDecimal(parts[0], 'latitude');
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
throw new Error(
`Invalid latitude "${latitude}". Latitude must be a number between -90 and 90.`,
);
}
const longitude = parseDecimal(parts[1], 'longitude');
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
throw new Error(
`Invalid longitude "${longitude}". Longitude must be a number between -180 and 180.`,
+33
View File
@@ -91,6 +91,25 @@ describe('emulation', () => {
it('throws when height is missing', () => {
assert.throws(() => viewportTransform('800'));
});
it('throws when viewport has unknown tags', () => {
assert.throws(() => viewportTransform('1280x720,mobile,bogus'));
});
it('throws when viewport has empty tags', () => {
assert.throws(() => viewportTransform('1280x720,'));
});
it('throws when viewport has extra dimension parts', () => {
assert.throws(() => viewportTransform('1280x720x2x9'));
});
it('throws when dimension parts are empty or contain whitespace', () => {
assert.throws(() => viewportTransform('800x'));
assert.throws(() => viewportTransform('x600'));
assert.throws(() => viewportTransform(' 1280 x 720'));
assert.throws(() => viewportTransform('1280x720, mobile'));
});
});
describe('geolocationTransform', () => {
@@ -120,6 +139,20 @@ describe('emulation', () => {
it('throws when longitude is missing', () => {
assert.throws(() => geolocationTransform('48.1'));
});
it('throws when geolocation has empty parts or trailing commas', () => {
assert.throws(() => geolocationTransform('37.77,'));
assert.throws(() => geolocationTransform(','));
});
it('throws when geolocation has extra parts', () => {
assert.throws(() => geolocationTransform('37.77,-122.42,9'));
});
it('throws on hex coordinates or surrounding whitespace', () => {
assert.throws(() => geolocationTransform('0x10,0x20'));
assert.throws(() => geolocationTransform(' 37.77 , -122.42 '));
});
});
});