fix: reject JSON arrays as 3p and WebMCP tool params (#2785)

## Summary
`execute_3p_developer_tool` and `execute_webmcp_tool` parse a JSON
string as params. Arrays pass `typeof parsed === 'object' && parsed !==
null`, so `[]` skipped the object check and failed later with a
misleading error (`Tool not found` or a Puppeteer private-field
TypeError).

Reject arrays the same way `wsHeaders` already does.

## Testing
- `npm run test tests/tools/thirdPartyDeveloper.test.ts
tests/tools/webmcp.test.ts`
- `npm run typecheck`
- `npm run check-format`

AI-assisted (Grok)

Co-authored-by: Wolfgang Beyer <wolfi@chromium.org>
This commit is contained in:
kevinandWolfgang Beyer authored and GitHub committed 2026-09-28 13:51:58 +00:00
1 parent c0349ce982
commit c14e616da8
4 files changed
+57 -4

No files matched your search

+5 -1
View File
@@ -85,7 +85,11 @@ export const executeThirdPartyDeveloperTool = definePageTool(() => ({
if (request.params.params) {
try {
const parsed = JSON.parse(request.params.params);
if (typeof parsed === 'object' && parsed !== null) {
if (
typeof parsed === 'object' &&
parsed !== null &&
!Array.isArray(parsed)
) {
params = parsed;
} else {
throw new Error('Parsed params is not an object');
+5 -1
View File
@@ -47,7 +47,11 @@ export const executeWebMcpTool = definePageTool(() => ({
if (request.params.input) {
try {
const parsed = JSON.parse(request.params.input);
if (typeof parsed === 'object' && parsed !== null) {
if (
typeof parsed === 'object' &&
parsed !== null &&
!Array.isArray(parsed)
) {
input = parsed;
} else {
throw new Error('Parsed input is not an object');
+24 -1
View File
@@ -5,7 +5,7 @@
*/
import assert from 'node:assert';
import {describe, it} from 'node:test';
import {afterEach, describe, it} from 'node:test';
import sinon from 'sinon';
@@ -18,9 +18,14 @@ import {
listThirdPartyDeveloperTools,
} from '../../src/tools/thirdPartyDeveloper.js';
import type {ToolGroups} from '../../src/tools/thirdPartyDeveloper.js';
import {createHandlerMocks} from '../mocks.js';
import {withMcpContext} from '../utils.js';
describe('thirdPartyDeveloperTools', () => {
afterEach(() => {
sinon.restore();
});
describe('list_3p_developer_tools', () => {
it('lists tools', async () => {
await withMcpContext(
@@ -391,6 +396,24 @@ describe('thirdPartyDeveloperTools', () => {
});
});
it('rejects JSON array params', async () => {
const {page, context, response, args} = createHandlerMocks();
await assert.rejects(
executeThirdPartyDeveloperTool(args).handler(
{
params: {
toolName: 'test-tool',
params: '[]',
},
page,
},
response,
context,
),
{message: /Parsed params is not an object/},
);
});
it('throws if parameters are invalid', async () => {
await withMcpContext(
async (response, context, args) => {
+23 -1
View File
@@ -5,14 +5,21 @@
*/
import assert from 'node:assert';
import {describe, it} from 'node:test';
import {afterEach, describe, it} from 'node:test';
import sinon from 'sinon';
import type {McpPage} from '../../src/McpPage.js';
import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
import {executeWebMcpTool} from '../../src/tools/webmcp.js';
import {createHandlerMocks} from '../mocks.js';
import {html, withMcpContext} from '../utils.js';
describe('webmcp', () => {
afterEach(() => {
sinon.restore();
});
describe('list_webmcp_tools', () => {
it('list webmcp tools in navigate_page response', async () => {
await withMcpContext(async (response, context, args) => {
@@ -110,6 +117,21 @@ describe('webmcp', () => {
);
});
it('rejects JSON array input', async () => {
const {page, context, response, args} = createHandlerMocks();
await assert.rejects(
executeWebMcpTool(args).handler(
{
params: {toolName: 'test_tool', input: '[]'},
page,
},
response,
context,
),
{message: /Parsed input is not an object/},
);
});
it('throws if input is invalid', async () => {
await withMcpContext(
async (response, context, args) => {