Root cause
Authentication & access control
fix(discord): use correct content_type property for audio attachment detection
How AI contributed: New attack surfacePublished Apr 3, 2026 in
openclaw/openclawAll 35 findings in this repository
Discord monitor preflight (src/discord/monitor/message-handler.preflight.ts) transcribed first audio attachment (transcribeFirstAudio) for mention detection in requireMention guilds BEFORE the member-access check, so untrusted guild voice notes were transcribed pre-auth; advisory fix gates the audio preflight on member access.
Root cause
fix(discord): use correct content_type property for audio attachment detection
Fix
Discord: gate audio preflight on member access (#57695)
Fix by Jacob Tomlinson · no AI marker found
Case facts
@@ -501,8 +501,8 @@ export async function preflightDiscordMessage( // Preflight audio transcription for mention detection in guilds // This allows voice notes to be checked for mentions before being dropped let preflightTranscript: string | undefined;- const hasAudioAttachment = message.attachments?.some((att: { contentType?: string }) =>- att.contentType?.startsWith("audio/"),+ const hasAudioAttachment = message.attachments?.some((att: { content_type?: string }) =>+ att.content_type?.startsWith("audio/"), ); const needsPreflightTranscription = !isDirectMessage &&@@ -516,18 +516,18 @@ export async function preflightDiscordMessage( const { transcribeFirstAudio } = await import("../../media-understanding/audio-preflight.js"); const audioPaths = message.attachments- ?.filter((att: { contentType?: string; url: string }) =>- att.contentType?.startsWith("audio/"),+ ?.filter((att: { content_type?: string; url: string }) =>+ att.content_type?.startsWith("audio/"), ) .map((att: { url: string }) => att.url) ?? []; if (audioPaths.length > 0) { const tempCtx = { MediaUrls: audioPaths, MediaTypes: message.attachments- ?.filter((att: { contentType?: string; url: string }) =>- att.contentType?.startsWith("audio/"),+ ?.filter((att: { content_type?: string; url: string }) =>+ att.content_type?.startsWith("audio/"), )- .map((att: { contentType?: string }) => att.contentType)+ .map((att: { content_type?: string }) => att.content_type) .filter(Boolean) as string[], }; preflightTranscript = await transcribeFirstAudio({@@ -679,9 +679,22 @@ export async function preflightDiscordMessage( shouldRequireMention: shouldRequireMentionByConfig, bypassMentionRequirement, });+ const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({+ channelConfig,+ guildInfo,+ memberRoleIds,+ sender,+ allowNameMatching,+ });++ if (isGuildMessage && hasAccessRestrictions && !memberAllowed) {+ logDebug(`[discord-preflight] drop: member not allowed`);+ // Keep stable Discord user IDs out of routine deny-path logs.+ logVerbose("Blocked discord guild sender (not in users/roles allowlist)");+ return null;+ } - // Preflight audio transcription for mention detection in guilds.- // This allows voice notes to be checked for mentions before being dropped.+ // Only authorized guild senders should reach the expensive transcription path. const { hasTypedText, transcript: preflightTranscript } = await resolveDiscordPreflightAudioMentionContext({ message,@@ -725,13 +738,6 @@ export async function preflightDiscordMessage( surface: "discord", }); const hasControlCommandInMessage = hasControlCommand(baseText, params.cfg);- const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({- channelConfig,- guildInfo,- memberRoleIds,- sender,- allowNameMatching,- }); if (!isDirectMessage) { const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({@@ -834,12 +840,6 @@ export async function preflightDiscordMessage( return null; } - if (isGuildMessage && hasAccessRestrictions && !memberAllowed) {- logDebug(`[discord-preflight] drop: member not allowed`);- logVerbose(`Blocked discord guild sender ${sender.id} (not in users/roles allowlist)`);- return null;- }- const systemLocation = resolveDiscordSystemLocation({ isDirectMessage, isGroupDm,@@ -765,6 +765,66 @@ describe("preflightDiscordMessage", () => { expect(result?.wasMentioned).toBe(true); }); + it("does not transcribe guild audio from unauthorized members", async () => {+ const channelId = "channel-audio-unauthorized-1";+ const guildId = "guild-audio-unauthorized-1";+ const client = createGuildTextClient(channelId);++ const message = createDiscordMessage({+ id: "m-audio-unauthorized-1",+ channelId,+ content: "",+ attachments: [+ {+ id: "att-1",+ url: "https://cdn.discordapp.com/attachments/voice.ogg",+ content_type: "audio/ogg",+ filename: "voice.ogg",+ },+ ],+ author: {+ id: "user-2",+ bot: false,+ username: "Mallory",+ },+ });++ const result = await preflightDiscordMessage({+ ...createPreflightArgs({+ cfg: {+ ...DEFAULT_PREFLIGHT_CFG,+ messages: {+ groupChat: {+ mentionPatterns: ["openclaw"],+ },+ },+ } as import("openclaw/plugin-sdk/config-runtime").OpenClawConfig,+ discordConfig: {} as DiscordConfig,+ data: createGuildEvent({+ channelId,+ guildId,+ author: message.author,+ message,+ }),+ client,+ }),+ guildEntries: {+ [guildId]: {+ channels: {+ [channelId]: {+ allow: true,+ requireMention: true,+ users: ["user-1"],+ },+ },+ },+ },+ });++ expect(transcribeFirstAudioMock).not.toHaveBeenCalled();+ expect(result).toBeNull();+ });+ it("drops guild message without mention when channel has configuredBinding and requireMention: true", async () => { const conversationRuntime = await import("openclaw/plugin-sdk/conversation-runtime"); const channelId = "ch-binding-1";AI-assisted change 9bd609676b6bca16b1b606bf5d72d7c917a51f21fbcf910428700cf79655c3ba · Fix ea44784629dd1a77499bd8afc19f0d4376059df8b074e783a8d8b90e53320429