From b00aecffba6ad902c2aa02a361689395d19faaa4 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 1 Apr 2026 14:16:50 -0400 Subject: [PATCH 1/2] chore: add CVE-2026-4926 entry to CHANGELOG Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f74b9..8c815e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +### Security + +- **CVE-2026-4926**: Upgraded `@modelcontextprotocol/sdk` to `^1.29.0`, resolving `path-to-regexp` to `8.4.1` and fixing the ReDoS vulnerability [GHSA-j3q9-mxjg-w52f](https://github.com/advisories/GHSA-j3q9-mxjg-w52f); regenerated output-validation patch for the new version + ### Public API - **Add `getAllTools` and `getVersionInfo` to public exports** — `getAllTools` is now re-exported from `@mapbox/mcp-devkit-server/tools` and `getVersionInfo` (plus `VersionInfo` type) from `@mapbox/mcp-devkit-server/utils`. These are needed by `hosted-mcp-server` to import server functionality via npm packages instead of submodule filesystem paths. From 8f4c11b237005223670760e80aaec62b90604828 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Mon, 22 Jun 2026 10:19:22 -0400 Subject: [PATCH 2/2] feat: validate style IDs in style_comparison_tool Add input validation for the before/after style parameters to ensure they contain only alphanumeric characters, hyphens, and underscores. Validation is enforced at both the Zod schema layer and inside processStyleId() after the mapbox://styles/ prefix is stripped, so malformed style IDs are rejected with a descriptive error before any comparison URL is constructed. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 4 ++ .../StyleComparisonTool.schema.ts | 8 +++ .../StyleComparisonTool.ts | 54 ++++++++++++------- .../StyleComparisonTool.test.ts | 30 +++++++++++ 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fc893..1721433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +### New Features + +- **Style ID validation for `style_comparison_tool`**: `before` and `after` inputs are now validated to contain only alphanumeric characters, hyphens, and underscores (after stripping the optional `mapbox://styles/` prefix). Validation is enforced at both the Zod schema layer and inside `processStyleId()`. Malformed style IDs are rejected with a descriptive error before any URL is constructed. + ## 0.8.0 - 2026-05-05 ## 0.7.5 - 2026-05-05 diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts b/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts index ec25e25..1265c11 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts @@ -6,11 +6,19 @@ import { z } from 'zod'; export const StyleComparisonSchema = z.object({ before: z .string() + .regex( + /^(?:mapbox:\/\/styles\/)?[a-zA-Z0-9_-]+(\/[a-zA-Z0-9_-]+)?$/, + 'Invalid style format. Use mapbox://styles/username/styleId, username/styleId, or a styleId containing only letters, numbers, hyphens, and underscores.' + ) .describe( 'Mapbox style for the "before" side. Accepts: full style URL (mapbox://styles/username/styleId), username/styleId format, or just styleId if using your own styles' ), after: z .string() + .regex( + /^(?:mapbox:\/\/styles\/)?[a-zA-Z0-9_-]+(\/[a-zA-Z0-9_-]+)?$/, + 'Invalid style format. Use mapbox://styles/username/styleId, username/styleId, or a styleId containing only letters, numbers, hyphens, and underscores.' + ) .describe( 'Mapbox style for the "after" side. Accepts: full style URL (mapbox://styles/username/styleId), username/styleId format, or just styleId if using your own styles' ), diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.ts b/src/tools/style-comparison-tool/StyleComparisonTool.ts index 82f7638..e916b5a 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.ts @@ -40,33 +40,49 @@ export class StyleComparisonTool extends BaseTool< super({ inputSchema: StyleComparisonSchema }); } + /** + * Validates that a resolved username/styleId contains only safe characters. + * Style IDs must be alphanumeric with hyphens and underscores only. + */ + private validateStyleId(resolved: string): void { + if (!/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/.test(resolved)) { + throw new Error( + `Invalid style ID format: "${resolved}". ` + + `Style IDs must be in username/styleId format using only letters, numbers, hyphens, and underscores.` + ); + } + } + /** * Processes style input to extract username/styleId format */ private processStyleId(style: string, accessToken: string): string { + let resolved: string; + // If it's a full URL, extract the username/styleId part if (style.startsWith('mapbox://styles/')) { - return style.replace('mapbox://styles/', ''); - } - - // If it contains a slash, assume it's already username/styleId format - if (style.includes('/')) { - return style; + resolved = style.replace('mapbox://styles/', ''); + } else if (style.includes('/')) { + // If it contains a slash, assume it's already username/styleId format + resolved = style; + } else { + // If it's just a style ID, try to get username from the token + try { + const username = getUserNameFromToken(accessToken); + resolved = `${username}/${style}`; + } catch (error) { + throw new Error( + `Could not determine username for style ID "${style}". ${error instanceof Error ? error.message : ''}\n` + + `Please provide either:\n` + + `1. Full style URL: mapbox://styles/username/${style}\n` + + `2. Username/styleId format: username/${style}\n` + + `3. Just the style ID with a valid Mapbox token that contains username information` + ); + } } - // If it's just a style ID, try to get username from the token - try { - const username = getUserNameFromToken(accessToken); - return `${username}/${style}`; - } catch (error) { - throw new Error( - `Could not determine username for style ID "${style}". ${error instanceof Error ? error.message : ''}\n` + - `Please provide either:\n` + - `1. Full style URL: mapbox://styles/username/${style}\n` + - `2. Username/styleId format: username/${style}\n` + - `3. Just the style ID with a valid Mapbox token that contains username information` - ); - } + this.validateStyleId(resolved); + return resolved; } protected async execute( diff --git a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts index e715011..5024af7 100644 --- a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts +++ b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts @@ -130,6 +130,36 @@ describe('StyleComparisonTool', () => { ).toContain('Invalid token type'); }); + it('should reject style IDs with invalid characters', async () => { + const input = { + before: 'mapbox/streets-v12', + after: 'bad', + accessToken: 'pk.test.token' + }; + + const result = await tool.run(input); + + expect(result.isError).toBe(true); + expect( + (result.content[0] as { type: 'text'; text: string }).text + ).toContain('Invalid style format'); + }); + + it('should reject style URLs with invalid characters after stripping scheme', async () => { + const input = { + before: 'mapbox://styles/mapbox/streets-v12', + after: 'mapbox://styles/bad/evil"style', + accessToken: 'pk.test.token' + }; + + const result = await tool.run(input); + + expect(result.isError).toBe(true); + expect( + (result.content[0] as { type: 'text'; text: string }).text + ).toContain('Invalid style format'); + }); + it('should return error for style ID without valid username in token', async () => { // Mock getUserNameFromToken to throw an error vi.spyOn(jwtUtils, 'getUserNameFromToken').mockImplementation(() => {