From f0a0766d99534c951197b56d89bf6df7ed2e23f5 Mon Sep 17 00:00:00 2001 From: Echo Date: Sun, 15 Feb 2026 01:36:43 -0500 Subject: [PATCH] fix(mattermost): validate JSON payload fields + normalize callbackPath - parseSlashCommandPayload JSON branch now validates required fields (token, team_id, channel_id, user_id, command) like the form-encoded branch, preventing runtime exceptions on malformed JSON payloads - normalizeCallbackPath() ensures leading '/' to prevent malformed URLs like 'http://host:portapi/...' when callbackPath lacks a leading slash - Applied in resolveSlashCommandConfig and resolveCallbackUrl Addresses Codex review round 3 (P2 items). --- .../src/mattermost/slash-commands.ts | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/extensions/mattermost/src/mattermost/slash-commands.ts b/extensions/mattermost/src/mattermost/slash-commands.ts index e08ff22e993..f284fcccff9 100644 --- a/extensions/mattermost/src/mattermost/slash-commands.ts +++ b/extensions/mattermost/src/mattermost/slash-commands.ts @@ -301,7 +301,32 @@ export function parseSlashCommandPayload( try { if (contentType?.includes("application/json")) { - return JSON.parse(body) as MattermostSlashCommandPayload; + const parsed = JSON.parse(body) as Record; + + // Validate required fields (same checks as the form-encoded branch) + const token = typeof parsed.token === "string" ? parsed.token : ""; + const teamId = typeof parsed.team_id === "string" ? parsed.team_id : ""; + const channelId = typeof parsed.channel_id === "string" ? parsed.channel_id : ""; + const userId = typeof parsed.user_id === "string" ? parsed.user_id : ""; + const command = typeof parsed.command === "string" ? parsed.command : ""; + + if (!token || !teamId || !channelId || !userId || !command) { + return null; + } + + return { + token, + team_id: teamId, + team_domain: typeof parsed.team_domain === "string" ? parsed.team_domain : undefined, + channel_id: channelId, + channel_name: typeof parsed.channel_name === "string" ? parsed.channel_name : undefined, + user_id: userId, + user_name: typeof parsed.user_name === "string" ? parsed.user_name : undefined, + command, + text: typeof parsed.text === "string" ? parsed.text : "", + trigger_id: typeof parsed.trigger_id === "string" ? parsed.trigger_id : undefined, + response_url: typeof parsed.response_url === "string" ? parsed.response_url : undefined, + }; } // Default: application/x-www-form-urlencoded @@ -349,13 +374,23 @@ export function resolveCommandText(trigger: string, text: string): string { const DEFAULT_CALLBACK_PATH = "/api/channels/mattermost/command"; +/** + * Ensure the callback path starts with a leading `/` to prevent + * malformed URLs like `http://host:portapi/...`. + */ +function normalizeCallbackPath(path: string): string { + const trimmed = path.trim(); + if (!trimmed) return DEFAULT_CALLBACK_PATH; + return trimmed.startsWith("/") ? trimmed : `/${trimmed}`; +} + export function resolveSlashCommandConfig( raw?: Partial, ): MattermostSlashCommandConfig { return { native: raw?.native ?? "auto", nativeSkills: raw?.nativeSkills ?? "auto", - callbackPath: raw?.callbackPath?.trim() || DEFAULT_CALLBACK_PATH, + callbackPath: normalizeCallbackPath(raw?.callbackPath ?? DEFAULT_CALLBACK_PATH), callbackUrl: raw?.callbackUrl?.trim() || undefined, }; } @@ -383,6 +418,6 @@ export function resolveCallbackUrl(params: { return params.config.callbackUrl; } const host = params.gatewayHost || "localhost"; - const path = params.config.callbackPath; + const path = normalizeCallbackPath(params.config.callbackPath); return `http://${host}:${params.gatewayPort}${path}`; }