OpenClaw < 2026.2.26 - Improper Authorization via DM Pairing Store Identity Inheritance in Group Allowlist
Description
OpenClaw versions prior to 2026.2.26 contain an authorization bypass vulnerability where DM pairing-store identities are incorrectly eligible for group allowlist authorization checks. Attackers can exploit this cross-context authorization flaw by using a sender approved via DM pairing to satisfy group sender allowlist checks without explicit presence in groupAllowFrom, bypassing group message access controls.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.2.26 | 2026.2.26 |
Affected products
1Patches
2051fdcc42812fix(security): centralize dm/group allowlist auth composition
8 files changed · +428 −108
extensions/mattermost/src/mattermost/monitor-auth.ts+1 −1 modified@@ -44,7 +44,7 @@ export function isMattermostSenderAllowed(params: { allowFrom: string[]; allowNameMatching?: boolean; }): boolean { - const allowFrom = params.allowFrom; + const allowFrom = normalizeMattermostAllowList(params.allowFrom); if (allowFrom.length === 0) { return false; }
extensions/mattermost/src/mattermost/monitor.ts+47 −38 modified@@ -37,11 +37,7 @@ import { type MattermostPost, type MattermostUser, } from "./client.js"; -import { - isMattermostSenderAllowed, - normalizeMattermostAllowList, - resolveMattermostEffectiveAllowFromLists, -} from "./monitor-auth.js"; +import { isMattermostSenderAllowed, normalizeMattermostAllowList } from "./monitor-auth.js"; import { createDedupeCache, formatInboundFromLabel, @@ -360,18 +356,32 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} senderId; const rawText = post.message?.trim() || ""; const dmPolicy = account.config.dmPolicy ?? "pairing"; + const normalizedAllowFrom = normalizeMattermostAllowList(account.config.allowFrom ?? []); + const normalizedGroupAllowFrom = normalizeMattermostAllowList( + account.config.groupAllowFrom ?? [], + ); const storeAllowFrom = normalizeMattermostAllowList( dmPolicy === "allowlist" ? [] : await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), ); - const { effectiveAllowFrom, effectiveGroupAllowFrom } = - resolveMattermostEffectiveAllowFromLists({ - dmPolicy, - allowFrom: account.config.allowFrom, - groupAllowFrom: account.config.groupAllowFrom, - storeAllowFrom, - }); + const accessDecision = resolveDmGroupAccessWithLists({ + isGroup: kind !== "direct", + dmPolicy, + groupPolicy, + allowFrom: normalizedAllowFrom, + groupAllowFrom: normalizedGroupAllowFrom, + storeAllowFrom, + isSenderAllowed: (allowFrom) => + isMattermostSenderAllowed({ + senderId, + senderName, + allowFrom, + allowNameMatching, + }), + }); + const effectiveAllowFrom = accessDecision.effectiveAllowFrom; + const effectiveGroupAllowFrom = accessDecision.effectiveGroupAllowFrom; const allowTextCommands = core.channel.commands.shouldHandleTextCommands({ cfg, surface: "mattermost", @@ -404,17 +414,15 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} hasControlCommand, }); const commandAuthorized = - kind === "direct" - ? dmPolicy === "open" || senderAllowedForCommands - : commandGate.commandAuthorized; + kind === "direct" ? accessDecision.decision === "allow" : commandGate.commandAuthorized; - if (kind === "direct") { - if (dmPolicy === "disabled") { - logVerboseMessage(`mattermost: drop dm (dmPolicy=disabled sender=${senderId})`); - return; - } - if (dmPolicy !== "open" && !senderAllowedForCommands) { - if (dmPolicy === "pairing") { + if (accessDecision.decision !== "allow") { + if (kind === "direct") { + if (accessDecision.reason === "dmPolicy=disabled") { + logVerboseMessage(`mattermost: drop dm (dmPolicy=disabled sender=${senderId})`); + return; + } + if (accessDecision.decision === "pairing") { const { code, created } = await core.channel.pairing.upsertPairingRequest({ channel: "mattermost", id: senderId, @@ -437,26 +445,27 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} logVerboseMessage(`mattermost: pairing reply failed for ${senderId}: ${String(err)}`); } } - } else { - logVerboseMessage(`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`); + return; } + logVerboseMessage(`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`); return; } - } else { - if (groupPolicy === "disabled") { + if (accessDecision.reason === "groupPolicy=disabled") { logVerboseMessage("mattermost: drop group message (groupPolicy=disabled)"); return; } - if (groupPolicy === "allowlist") { - if (effectiveGroupAllowFrom.length === 0) { - logVerboseMessage("mattermost: drop group message (no group allowlist)"); - return; - } - if (!groupAllowedForCommands) { - logVerboseMessage(`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`); - return; - } + if (accessDecision.reason === "groupPolicy=allowlist (empty allowlist)") { + logVerboseMessage("mattermost: drop group message (no group allowlist)"); + return; + } + if (accessDecision.reason === "groupPolicy=allowlist (not allowlisted)") { + logVerboseMessage(`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`); + return; } + logVerboseMessage( + `mattermost: drop group message (groupPolicy=${groupPolicy} reason=${accessDecision.reason})`, + ); + return; } if (kind !== "direct" && commandGate.shouldBlock) { @@ -852,14 +861,14 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} isGroup: kind !== "direct", dmPolicy, groupPolicy, - allowFrom: account.config.allowFrom, - groupAllowFrom: account.config.groupAllowFrom, + allowFrom: normalizeMattermostAllowList(account.config.allowFrom ?? []), + groupAllowFrom: normalizeMattermostAllowList(account.config.groupAllowFrom ?? []), storeAllowFrom, isSenderAllowed: (allowFrom) => isMattermostSenderAllowed({ senderId: userId, senderName, - allowFrom: normalizeMattermostAllowList(allowFrom), + allowFrom, allowNameMatching, }), });
extensions/msteams/src/monitor-handler/message-handler.ts+1 −4 modified@@ -146,18 +146,15 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { }); const effectiveDmAllowFrom = resolvedAllowFromLists.effectiveAllowFrom; if (isDirectMessage && msteamsCfg) { - const allowFrom = dmAllowFrom; - if (dmPolicy === "disabled") { log.debug?.("dropping dm (dms disabled)"); return; } if (dmPolicy !== "open") { - const effectiveAllowFrom = [...allowFrom.map((v) => String(v)), ...storedAllowFrom]; const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg); const allowMatch = resolveMSTeamsAllowlistMatch({ - allowFrom: effectiveAllowFrom, + allowFrom: effectiveDmAllowFrom, senderId, senderName, allowNameMatching,
package.json+2 −1 modified@@ -54,7 +54,7 @@ "build": "pnpm canvas:a2ui:bundle && tsdown && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-compat.ts", "build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json", "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh", - "check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries", + "check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:auth:no-pairing-store-group", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-links", "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500", "deadcode:ci": "pnpm deadcode:report:ci:knip && pnpm deadcode:report:ci:ts-prune && pnpm deadcode:report:ci:ts-unused", @@ -89,6 +89,7 @@ "ios:run": "bash -lc './scripts/ios-configure-signing.sh && cd apps/ios && xcodegen generate && xcodebuild -project OpenClaw.xcodeproj -scheme OpenClaw -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build && xcrun simctl boot \"${IOS_SIM:-iPhone 17}\" || true && xcrun simctl launch booted ai.openclaw.ios'", "lint": "oxlint --type-aware", "lint:all": "pnpm lint && pnpm lint:swift", + "lint:auth:no-pairing-store-group": "node scripts/check-no-pairing-store-group-auth.mjs", "lint:docs": "pnpm dlx markdownlint-cli2", "lint:docs:fix": "pnpm dlx markdownlint-cli2 --fix", "lint:fix": "oxlint --type-aware --fix && pnpm format",
scripts/check-no-pairing-store-group-auth.mjs+227 −0 added@@ -0,0 +1,227 @@ +#!/usr/bin/env node + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")]; + +const allowedFiles = new Set([ + path.join(repoRoot, "src", "security", "dm-policy-shared.ts"), + path.join(repoRoot, "src", "channels", "allow-from.ts"), + // Config migration/audit logic may intentionally reference store + group fields. + path.join(repoRoot, "src", "security", "fix.ts"), + path.join(repoRoot, "src", "security", "audit-channel.ts"), +]); + +const storeIdentifierRe = /^(?:storeAllowFrom|storedAllowFrom|storeAllowList)$/i; +const groupNameRe = + /(?:groupAllowFrom|effectiveGroupAllowFrom|groupAllowed|groupAllow|groupAuth|groupSender)/i; +const allowedResolverCallNames = new Set([ + "resolveEffectiveAllowFromLists", + "resolveDmGroupAccessWithLists", + "resolveMattermostEffectiveAllowFromLists", + "resolveIrcEffectiveAllowlists", +]); + +function isTestLikeFile(filePath) { + return ( + filePath.endsWith(".test.ts") || + filePath.endsWith(".test-utils.ts") || + filePath.endsWith(".test-harness.ts") || + filePath.endsWith(".e2e-harness.ts") + ); +} + +async function collectTypeScriptFiles(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const out = []; + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + out.push(...(await collectTypeScriptFiles(entryPath))); + continue; + } + if (!entry.isFile() || !entryPath.endsWith(".ts") || isTestLikeFile(entryPath)) { + continue; + } + out.push(entryPath); + } + return out; +} + +function toLine(sourceFile, node) { + return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; +} + +function getPropertyNameText(name) { + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { + return name.text; + } + return null; +} + +function getDeclarationNameText(name) { + if (ts.isIdentifier(name)) { + return name.text; + } + if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) { + return name.getText(); + } + return null; +} + +function containsStoreIdentifier(node) { + let found = false; + const visit = (current) => { + if (found) { + return; + } + if (ts.isIdentifier(current) && storeIdentifierRe.test(current.text)) { + found = true; + return; + } + ts.forEachChild(current, visit); + }; + visit(node); + return found; +} + +function getCallName(node) { + if (!ts.isCallExpression(node)) { + return null; + } + if (ts.isIdentifier(node.expression)) { + return node.expression.text; + } + if (ts.isPropertyAccessExpression(node.expression)) { + return node.expression.name.text; + } + return null; +} + +function isSuspiciousNormalizeWithStoreCall(node) { + if (!ts.isCallExpression(node)) { + return false; + } + if (!ts.isIdentifier(node.expression) || node.expression.text !== "normalizeAllowFromWithStore") { + return false; + } + const firstArg = node.arguments[0]; + if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) { + return false; + } + let hasStoreProp = false; + let hasGroupAllowProp = false; + for (const property of firstArg.properties) { + if (!ts.isPropertyAssignment(property)) { + continue; + } + const name = getPropertyNameText(property.name); + if (!name) { + continue; + } + if (name === "storeAllowFrom" && containsStoreIdentifier(property.initializer)) { + hasStoreProp = true; + } + if (name === "allowFrom" && groupNameRe.test(property.initializer.getText())) { + hasGroupAllowProp = true; + } + } + return hasStoreProp && hasGroupAllowProp; +} + +function findViolations(content, filePath) { + const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); + const violations = []; + + const visit = (node) => { + if (ts.isVariableDeclaration(node) && node.initializer) { + const name = getDeclarationNameText(node.name); + if (name && groupNameRe.test(name) && containsStoreIdentifier(node.initializer)) { + const callName = getCallName(node.initializer); + if (callName && allowedResolverCallNames.has(callName)) { + ts.forEachChild(node, visit); + return; + } + violations.push({ + line: toLine(sourceFile, node), + reason: `group-scoped variable "${name}" references pairing-store identifiers`, + }); + } + } + + if (ts.isPropertyAssignment(node)) { + const propName = getPropertyNameText(node.name); + if (propName && groupNameRe.test(propName) && containsStoreIdentifier(node.initializer)) { + violations.push({ + line: toLine(sourceFile, node), + reason: `group-scoped property "${propName}" references pairing-store identifiers`, + }); + } + } + + if (isSuspiciousNormalizeWithStoreCall(node)) { + violations.push({ + line: toLine(sourceFile, node), + reason: "group allowlist uses normalizeAllowFromWithStore(...) with pairing-store entries", + }); + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return violations; +} + +async function main() { + const files = ( + await Promise.all(sourceRoots.map(async (root) => await collectTypeScriptFiles(root))) + ).flat(); + + const violations = []; + for (const filePath of files) { + if (allowedFiles.has(filePath)) { + continue; + } + const content = await fs.readFile(filePath, "utf8"); + const fileViolations = findViolations(content, filePath); + for (const violation of fileViolations) { + violations.push({ + path: path.relative(repoRoot, filePath), + ...violation, + }); + } + } + + if (violations.length === 0) { + return; + } + + console.error("Found pairing-store identifiers referenced in group auth composition:"); + for (const violation of violations) { + console.error(`- ${violation.path}:${violation.line} (${violation.reason})`); + } + console.error( + "Group auth must be composed via shared resolvers (resolveDmGroupAccessWithLists / resolveEffectiveAllowFromLists).", + ); + process.exit(1); +} + +const isDirectExecution = (() => { + const entry = process.argv[1]; + if (!entry) { + return false; + } + return path.resolve(entry) === fileURLToPath(import.meta.url); +})(); + +if (isDirectExecution) { + main().catch((error) => { + console.error(error); + process.exit(1); + }); +}
src/imessage/monitor/inbound-processing.ts+43 −54 modified@@ -20,7 +20,7 @@ import { resolveChannelGroupRequireMention, } from "../../config/group-policy.js"; import { resolveAgentRoute } from "../../routing/resolve-route.js"; -import { resolveEffectiveAllowFromLists } from "../../security/dm-policy-shared.js"; +import { resolveDmGroupAccessWithLists } from "../../security/dm-policy-shared.js"; import { truncateUtf16Safe } from "../../utils.js"; import { formatIMessageChatTarget, @@ -139,72 +139,61 @@ export function resolveIMessageInboundDecision(params: { } const groupId = isGroup ? groupIdCandidate : undefined; - const { effectiveAllowFrom: effectiveDmAllowFrom, effectiveGroupAllowFrom } = - resolveEffectiveAllowFromLists({ - allowFrom: params.allowFrom, - groupAllowFrom: params.groupAllowFrom, - storeAllowFrom: params.storeAllowFrom, - dmPolicy: params.dmPolicy, - groupAllowFromFallbackToAllowFrom: false, - }); + const accessDecision = resolveDmGroupAccessWithLists({ + isGroup, + dmPolicy: params.dmPolicy, + groupPolicy: params.groupPolicy, + allowFrom: params.allowFrom, + groupAllowFrom: params.groupAllowFrom, + storeAllowFrom: params.storeAllowFrom, + groupAllowFromFallbackToAllowFrom: false, + isSenderAllowed: (allowFrom) => + isAllowedIMessageSender({ + allowFrom, + sender, + chatId, + chatGuid, + chatIdentifier, + }), + }); + const effectiveDmAllowFrom = accessDecision.effectiveAllowFrom; + const effectiveGroupAllowFrom = accessDecision.effectiveGroupAllowFrom; + const dmAuthorized = !isGroup && accessDecision.decision === "allow"; - if (isGroup) { - if (params.groupPolicy === "disabled") { - params.logVerbose?.("Blocked iMessage group message (groupPolicy: disabled)"); - return { kind: "drop", reason: "groupPolicy disabled" }; - } - if (params.groupPolicy === "allowlist") { - if (effectiveGroupAllowFrom.length === 0) { + if (accessDecision.decision !== "allow") { + if (isGroup) { + if (accessDecision.reason === "groupPolicy=disabled") { + params.logVerbose?.("Blocked iMessage group message (groupPolicy: disabled)"); + return { kind: "drop", reason: "groupPolicy disabled" }; + } + if (accessDecision.reason === "groupPolicy=allowlist (empty allowlist)") { params.logVerbose?.( "Blocked iMessage group message (groupPolicy: allowlist, no groupAllowFrom)", ); return { kind: "drop", reason: "groupPolicy allowlist (empty groupAllowFrom)" }; } - const allowed = isAllowedIMessageSender({ - allowFrom: effectiveGroupAllowFrom, - sender, - chatId, - chatGuid, - chatIdentifier, - }); - if (!allowed) { + if (accessDecision.reason === "groupPolicy=allowlist (not allowlisted)") { params.logVerbose?.(`Blocked iMessage sender ${sender} (not in groupAllowFrom)`); return { kind: "drop", reason: "not in groupAllowFrom" }; } + params.logVerbose?.(`Blocked iMessage group message (${accessDecision.reason})`); + return { kind: "drop", reason: accessDecision.reason }; } - if (groupListPolicy.allowlistEnabled && !groupListPolicy.allowed) { - params.logVerbose?.( - `imessage: skipping group message (${groupId ?? "unknown"}) not in allowlist`, - ); - return { kind: "drop", reason: "group id not in allowlist" }; - } - } - - const dmHasWildcard = effectiveDmAllowFrom.includes("*"); - const dmAuthorized = - params.dmPolicy === "open" - ? true - : dmHasWildcard || - (effectiveDmAllowFrom.length > 0 && - isAllowedIMessageSender({ - allowFrom: effectiveDmAllowFrom, - sender, - chatId, - chatGuid, - chatIdentifier, - })); - - if (!isGroup) { - if (params.dmPolicy === "disabled") { + if (accessDecision.reason === "dmPolicy=disabled") { return { kind: "drop", reason: "dmPolicy disabled" }; } - if (!dmAuthorized) { - if (params.dmPolicy === "pairing") { - return { kind: "pairing", senderId: senderNormalized }; - } - params.logVerbose?.(`Blocked iMessage sender ${sender} (dmPolicy=${params.dmPolicy})`); - return { kind: "drop", reason: "dmPolicy blocked" }; + if (accessDecision.decision === "pairing") { + return { kind: "pairing", senderId: senderNormalized }; } + params.logVerbose?.(`Blocked iMessage sender ${sender} (dmPolicy=${params.dmPolicy})`); + return { kind: "drop", reason: "dmPolicy blocked" }; + } + + if (isGroup && groupListPolicy.allowlistEnabled && !groupListPolicy.allowed) { + params.logVerbose?.( + `imessage: skipping group message (${groupId ?? "unknown"}) not in allowlist`, + ); + return { kind: "drop", reason: "group id not in allowlist" }; } const route = resolveAgentRoute({
src/security/dm-policy-channel-smoke.test.ts+65 −0 added@@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { isAllowedBlueBubblesSender } from "../../extensions/bluebubbles/src/targets.js"; +import { isMattermostSenderAllowed } from "../../extensions/mattermost/src/mattermost/monitor-auth.js"; +import { isSignalSenderAllowed, type SignalSender } from "../signal/identity.js"; +import { resolveDmGroupAccessWithLists } from "./dm-policy-shared.js"; + +type ChannelSmokeCase = { + name: string; + storeAllowFrom: string[]; + isSenderAllowed: (allowFrom: string[]) => boolean; +}; + +const signalSender: SignalSender = { + kind: "phone", + raw: "+15550001111", + e164: "+15550001111", +}; + +const cases: ChannelSmokeCase[] = [ + { + name: "bluebubbles", + storeAllowFrom: ["attacker-user"], + isSenderAllowed: (allowFrom) => + isAllowedBlueBubblesSender({ + allowFrom, + sender: "attacker-user", + chatId: 101, + }), + }, + { + name: "signal", + storeAllowFrom: [signalSender.e164], + isSenderAllowed: (allowFrom) => isSignalSenderAllowed(signalSender, allowFrom), + }, + { + name: "mattermost", + storeAllowFrom: ["user:attacker-user"], + isSenderAllowed: (allowFrom) => + isMattermostSenderAllowed({ + senderId: "attacker-user", + senderName: "Attacker", + allowFrom, + }), + }, +]; + +describe("security/dm-policy-shared channel smoke", () => { + for (const testCase of cases) { + for (const ingress of ["message", "reaction"] as const) { + it(`[${testCase.name}] blocks group ${ingress} when sender is only in pairing store`, () => { + const access = resolveDmGroupAccessWithLists({ + isGroup: true, + dmPolicy: "pairing", + groupPolicy: "allowlist", + allowFrom: ["owner-user"], + groupAllowFrom: ["group-owner"], + storeAllowFrom: testCase.storeAllowFrom, + isSenderAllowed: testCase.isSenderAllowed, + }); + expect(access.decision).toBe("block"); + expect(access.reason).toBe("groupPolicy=allowlist (not allowlisted)"); + }); + } + } +});
src/security/dm-policy-shared.test.ts+42 −10 modified@@ -133,56 +133,88 @@ describe("security/dm-policy-shared", () => { const cases = [ { name: "dmPolicy=open", + isGroup: false, dmPolicy: "open" as const, + groupPolicy: "allowlist" as const, allowFrom: [] as string[], - senderAllowed: false, + groupAllowFrom: [] as string[], + storeAllowFrom: [] as string[], + isSenderAllowed: () => false, expectedDecision: "allow" as const, expectedReactionAllowed: true, }, { name: "dmPolicy=disabled", + isGroup: false, dmPolicy: "disabled" as const, + groupPolicy: "allowlist" as const, allowFrom: [] as string[], - senderAllowed: false, + groupAllowFrom: [] as string[], + storeAllowFrom: [] as string[], + isSenderAllowed: () => false, expectedDecision: "block" as const, expectedReactionAllowed: false, }, { name: "dmPolicy=allowlist unauthorized", + isGroup: false, dmPolicy: "allowlist" as const, + groupPolicy: "allowlist" as const, allowFrom: ["owner"], - senderAllowed: false, + groupAllowFrom: [] as string[], + storeAllowFrom: [] as string[], + isSenderAllowed: () => false, expectedDecision: "block" as const, expectedReactionAllowed: false, }, { name: "dmPolicy=allowlist authorized", + isGroup: false, dmPolicy: "allowlist" as const, + groupPolicy: "allowlist" as const, allowFrom: ["owner"], - senderAllowed: true, + groupAllowFrom: [] as string[], + storeAllowFrom: [] as string[], + isSenderAllowed: () => true, expectedDecision: "allow" as const, expectedReactionAllowed: true, }, { name: "dmPolicy=pairing unauthorized", + isGroup: false, dmPolicy: "pairing" as const, + groupPolicy: "allowlist" as const, allowFrom: [] as string[], - senderAllowed: false, + groupAllowFrom: [] as string[], + storeAllowFrom: [] as string[], + isSenderAllowed: () => false, expectedDecision: "pairing" as const, expectedReactionAllowed: false, }, + { + name: "groupPolicy=allowlist rejects DM-paired sender not in explicit group list", + isGroup: true, + dmPolicy: "pairing" as const, + groupPolicy: "allowlist" as const, + allowFrom: ["owner"] as string[], + groupAllowFrom: ["group-owner"] as string[], + storeAllowFrom: ["paired-user"] as string[], + isSenderAllowed: (allowFrom: string[]) => allowFrom.includes("paired-user"), + expectedDecision: "block" as const, + expectedReactionAllowed: false, + }, ]; for (const channel of channels) { for (const testCase of cases) { const access = resolveDmGroupAccessWithLists({ - isGroup: false, + isGroup: testCase.isGroup, dmPolicy: testCase.dmPolicy, - groupPolicy: "allowlist", + groupPolicy: testCase.groupPolicy, allowFrom: testCase.allowFrom, - groupAllowFrom: [], - storeAllowFrom: [], - isSenderAllowed: () => testCase.senderAllowed, + groupAllowFrom: testCase.groupAllowFrom, + storeAllowFrom: testCase.storeAllowFrom, + isSenderAllowed: testCase.isSenderAllowed, }); const reactionAllowed = access.decision === "allow"; expect(access.decision, `[${channel}] ${testCase.name}`).toBe(testCase.expectedDecision);
8bdda7a651c2fix(security): keep DM pairing allowlists out of group auth
15 files changed · +194 −54
CHANGELOG.md+1 −0 modified@@ -95,6 +95,7 @@ Docs: https://docs.openclaw.ai - Security/Slack member + message subtype events: gate `member_*` plus `message_changed`/`message_deleted`/`thread_broadcast` system-event enqueue through shared sender authorization so DM `dmPolicy`/`allowFrom` and channel `users` allowlists are enforced consistently for non-message ingress; message subtype system events now fail closed when sender identity is missing, with regression coverage. This ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting. - Security/Telegram reactions: enforce `dmPolicy`/`allowFrom` and group allowlist authorization on `message_reaction` events before enqueueing reaction system events, preventing unauthorized reaction-triggered input in DMs and groups; ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting. - Security/Telegram group allowlist: fail closed for group sender authorization by removing DM pairing-store fallback from group allowlist evaluation; group sender access now requires explicit `groupAllowFrom` or per-group/per-topic `allowFrom`. (#25988) Thanks @bmendonca3. +- Security/DM-group allowlist boundaries: keep DM pairing-store approvals DM-only by removing pairing-store inheritance from group sender authorization in LINE and Mattermost message preflight, and by centralizing shared DM/group allowlist composition so group checks never include pairing-store entries. This ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting. - Security/Slack interactions: enforce channel/DM authorization and modal actor binding (`private_metadata.userId`) before enqueueing `block_action`/`view_submission`/`view_closed` system events, with regression coverage for unauthorized senders and missing/mismatched actor metadata. This ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting. - Security/Nextcloud Talk: drop replayed signed webhook events with persistent per-account replay dedupe across restarts, and reject unexpected webhook backend origins when account base URL is configured. Thanks @aristorechina for reporting. - Security/Nextcloud Talk: reject unsigned webhook traffic before full body reads, reducing unauthenticated request-body exposure, with auth-order regression coverage. (#26118) Thanks @bmendonca3.
docs/channels/groups.md+1 −0 modified@@ -184,6 +184,7 @@ Notes: - `groupPolicy` is separate from mention-gating (which requires @mentions). - WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`). +- DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists. - Discord: allowlist uses `channels.discord.guilds.<id>.channels`. - Slack: allowlist uses `channels.slack.channels`. - Matrix: allowlist uses `channels.matrix.groups` (room IDs, aliases, or names). Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported.
extensions/mattermost/src/mattermost/monitor.authz.test.ts+37 −0 added@@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { resolveMattermostEffectiveAllowFromLists } from "./monitor.js"; + +describe("mattermost monitor authz", () => { + it("keeps DM allowlist merged with pairing-store entries", () => { + const resolved = resolveMattermostEffectiveAllowFromLists({ + dmPolicy: "pairing", + allowFrom: ["@trusted-user"], + groupAllowFrom: ["@group-owner"], + storeAllowFrom: ["user:attacker"], + }); + + expect(resolved.effectiveAllowFrom).toEqual(["trusted-user", "attacker"]); + }); + + it("uses explicit groupAllowFrom without pairing-store inheritance", () => { + const resolved = resolveMattermostEffectiveAllowFromLists({ + dmPolicy: "pairing", + allowFrom: ["@trusted-user"], + groupAllowFrom: ["@group-owner"], + storeAllowFrom: ["user:attacker"], + }); + + expect(resolved.effectiveGroupAllowFrom).toEqual(["group-owner"]); + }); + + it("does not inherit pairing-store entries into group allowlist", () => { + const resolved = resolveMattermostEffectiveAllowFromLists({ + dmPolicy: "pairing", + allowFrom: ["@trusted-user"], + storeAllowFrom: ["user:attacker"], + }); + + expect(resolved.effectiveAllowFrom).toEqual(["trusted-user", "attacker"]); + expect(resolved.effectiveGroupAllowFrom).toEqual(["trusted-user"]); + }); +});
extensions/mattermost/src/mattermost/monitor.ts+25 −9 modified@@ -18,6 +18,7 @@ import { isDangerousNameMatchingEnabled, resolveControlCommandGate, resolveDmGroupAccessWithLists, + resolveEffectiveAllowFromLists, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, resolveChannelMediaMaxBytes, @@ -150,6 +151,23 @@ function normalizeAllowList(entries: Array<string | number>): string[] { return Array.from(new Set(normalized)); } +export function resolveMattermostEffectiveAllowFromLists(params: { + allowFrom?: Array<string | number> | null; + groupAllowFrom?: Array<string | number> | null; + storeAllowFrom?: Array<string | number> | null; + dmPolicy?: string | null; +}): { + effectiveAllowFrom: string[]; + effectiveGroupAllowFrom: string[]; +} { + return resolveEffectiveAllowFromLists({ + allowFrom: normalizeAllowList(params.allowFrom ?? []), + groupAllowFrom: normalizeAllowList(params.groupAllowFrom ?? []), + storeAllowFrom: normalizeAllowList(params.storeAllowFrom ?? []), + dmPolicy: params.dmPolicy, + }); +} + function isSenderAllowed(params: { senderId: string; senderName?: string; @@ -400,20 +418,18 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} senderId; const rawText = post.message?.trim() || ""; const dmPolicy = account.config.dmPolicy ?? "pairing"; - const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []); - const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []); const storeAllowFrom = normalizeAllowList( dmPolicy === "allowlist" ? [] : await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), ); - const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom])); - const effectiveGroupAllowFrom = Array.from( - new Set([ - ...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom), - ...storeAllowFrom, - ]), - ); + const { effectiveAllowFrom, effectiveGroupAllowFrom } = + resolveMattermostEffectiveAllowFromLists({ + dmPolicy, + allowFrom: account.config.allowFrom, + groupAllowFrom: account.config.groupAllowFrom, + storeAllowFrom, + }); const allowTextCommands = core.channel.commands.shouldHandleTextCommands({ cfg, surface: "mattermost",
src/channels/allow-from.test.ts+30 −5 modified@@ -1,10 +1,15 @@ import { describe, expect, it } from "vitest"; -import { firstDefined, isSenderIdAllowed, mergeAllowFromSources } from "./allow-from.js"; +import { + firstDefined, + isSenderIdAllowed, + mergeDmAllowFromSources, + resolveGroupAllowFromSources, +} from "./allow-from.js"; -describe("mergeAllowFromSources", () => { +describe("mergeDmAllowFromSources", () => { it("merges, trims, and filters empty values", () => { expect( - mergeAllowFromSources({ + mergeDmAllowFromSources({ allowFrom: [" line:user:abc ", "", 123], storeAllowFrom: [" ", "telegram:456"], }), @@ -13,7 +18,7 @@ describe("mergeAllowFromSources", () => { it("excludes pairing-store entries when dmPolicy is allowlist", () => { expect( - mergeAllowFromSources({ + mergeDmAllowFromSources({ allowFrom: ["+1111"], storeAllowFrom: ["+2222", "+3333"], dmPolicy: "allowlist", @@ -23,7 +28,7 @@ describe("mergeAllowFromSources", () => { it("keeps pairing-store entries for non-allowlist policies", () => { expect( - mergeAllowFromSources({ + mergeDmAllowFromSources({ allowFrom: ["+1111"], storeAllowFrom: ["+2222"], dmPolicy: "pairing", @@ -32,6 +37,26 @@ describe("mergeAllowFromSources", () => { }); }); +describe("resolveGroupAllowFromSources", () => { + it("prefers explicit group allowlist", () => { + expect( + resolveGroupAllowFromSources({ + allowFrom: ["owner"], + groupAllowFrom: ["group-owner", " group-admin "], + }), + ).toEqual(["group-owner", "group-admin"]); + }); + + it("falls back to DM allowlist when group allowlist is unset/empty", () => { + expect( + resolveGroupAllowFromSources({ + allowFrom: [" owner ", "", "owner2"], + groupAllowFrom: [], + }), + ).toEqual(["owner", "owner2"]); + }); +}); + describe("firstDefined", () => { it("returns the first non-undefined value", () => { expect(firstDefined(undefined, undefined, "x", "y")).toBe("x");
src/channels/allow-from.ts+13 −2 modified@@ -1,6 +1,6 @@ -export function mergeAllowFromSources(params: { +export function mergeDmAllowFromSources(params: { allowFrom?: Array<string | number>; - storeAllowFrom?: string[]; + storeAllowFrom?: Array<string | number>; dmPolicy?: string; }): string[] { const storeEntries = params.dmPolicy === "allowlist" ? [] : (params.storeAllowFrom ?? []); @@ -9,6 +9,17 @@ export function mergeAllowFromSources(params: { .filter(Boolean); } +export function resolveGroupAllowFromSources(params: { + allowFrom?: Array<string | number>; + groupAllowFrom?: Array<string | number>; +}): string[] { + const scoped = + params.groupAllowFrom && params.groupAllowFrom.length > 0 + ? params.groupAllowFrom + : (params.allowFrom ?? []); + return scoped.map((value) => String(value).trim()).filter(Boolean); +} + export function firstDefined<T>(...values: Array<T | undefined>) { for (const value of values) { if (typeof value !== "undefined") {
src/line/bot-access.ts+7 −3 modified@@ -1,4 +1,8 @@ -import { firstDefined, isSenderIdAllowed, mergeAllowFromSources } from "../channels/allow-from.js"; +import { + firstDefined, + isSenderIdAllowed, + mergeDmAllowFromSources, +} from "../channels/allow-from.js"; export type NormalizedAllowFrom = { entries: string[]; @@ -27,11 +31,11 @@ export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAll }; }; -export const normalizeAllowFromWithStore = (params: { +export const normalizeDmAllowFromWithStore = (params: { allowFrom?: Array<string | number>; storeAllowFrom?: string[]; dmPolicy?: string; -}): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params)); +}): NormalizedAllowFrom => normalizeAllowFrom(mergeDmAllowFromSources(params)); export const isSenderAllowed = (params: { allow: NormalizedAllowFrom;
src/line/bot-handlers.test.ts+35 −0 modified@@ -182,6 +182,41 @@ describe("handleLineWebhookEvents", () => { expect(processMessage).toHaveBeenCalledTimes(1); }); + it("blocks group sender that is only present in pairing-store allowlist", async () => { + const processMessage = vi.fn(); + readAllowFromStoreMock.mockResolvedValueOnce(["user-paired"]); + const event = { + type: "message", + message: { id: "m3b", type: "text", text: "hi" }, + replyToken: "reply-token", + timestamp: Date.now(), + source: { type: "group", groupId: "group-1", userId: "user-paired" }, + mode: "active", + webhookEventId: "evt-3b", + deliveryContext: { isRedelivery: false }, + } as MessageEvent; + + await handleLineWebhookEvents([event], { + cfg: { + channels: { line: { groupPolicy: "allowlist", groupAllowFrom: ["user-owner"] } }, + }, + account: { + accountId: "default", + enabled: true, + channelAccessToken: "token", + channelSecret: "secret", + tokenSource: "config", + config: { groupPolicy: "allowlist", groupAllowFrom: ["user-owner"] }, + }, + runtime: createRuntime(), + mediaMaxBytes: 1, + processMessage, + }); + + expect(buildLineMessageContextMock).not.toHaveBeenCalled(); + expect(processMessage).not.toHaveBeenCalled(); + }); + it("blocks group messages when wildcard group config disables groups", async () => { const processMessage = vi.fn(); const event = {
src/line/bot-handlers.ts+10 −7 modified@@ -21,7 +21,12 @@ import { upsertChannelPairingRequest, } from "../pairing/pairing-store.js"; import type { RuntimeEnv } from "../runtime.js"; -import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; +import { + firstDefined, + isSenderAllowed, + normalizeAllowFrom, + normalizeDmAllowFromWithStore, +} from "./bot-access.js"; import { getLineSourceInfo, buildLineMessageContext, @@ -117,7 +122,7 @@ async function shouldProcessLineEvent( const dmPolicy = account.config.dmPolicy ?? "pairing"; const storeAllowFrom = await readChannelAllowFromStore("line").catch(() => []); - const effectiveDmAllow = normalizeAllowFromWithStore({ + const effectiveDmAllow = normalizeDmAllowFromWithStore({ allowFrom: account.config.allowFrom, storeAllowFrom, dmPolicy, @@ -132,11 +137,9 @@ async function shouldProcessLineEvent( account.config.groupAllowFrom, fallbackGroupAllowFrom, ); - const effectiveGroupAllow = normalizeAllowFromWithStore({ - allowFrom: groupAllowFrom, - storeAllowFrom, - dmPolicy, - }); + // Group authorization stays explicit to group allowlists and must not + // inherit DM pairing-store identities. + const effectiveGroupAllow = normalizeAllowFrom(groupAllowFrom); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
src/security/dm-policy-shared.test.ts+5 −5 modified@@ -41,7 +41,7 @@ describe("security/dm-policy-shared", () => { storeAllowFrom: [" owner3 ", ""], }); expect(lists.effectiveAllowFrom).toEqual(["owner", "owner2", "owner3"]); - expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc", "owner3"]); + expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc"]); }); it("falls back to DM allowlist for groups when groupAllowFrom is empty", () => { @@ -51,7 +51,7 @@ describe("security/dm-policy-shared", () => { storeAllowFrom: [" owner2 "], }); expect(lists.effectiveAllowFrom).toEqual(["owner", "owner2"]); - expect(lists.effectiveGroupAllowFrom).toEqual(["owner", "owner2"]); + expect(lists.effectiveGroupAllowFrom).toEqual(["owner"]); }); it("excludes storeAllowFrom when dmPolicy is allowlist", () => { @@ -65,15 +65,15 @@ describe("security/dm-policy-shared", () => { expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc"]); }); - it("includes storeAllowFrom when dmPolicy is pairing", () => { + it("keeps group allowlist explicit when dmPolicy is pairing", () => { const lists = resolveEffectiveAllowFromLists({ allowFrom: ["+1111"], groupAllowFrom: [], storeAllowFrom: ["+2222"], dmPolicy: "pairing", }); expect(lists.effectiveAllowFrom).toEqual(["+1111", "+2222"]); - expect(lists.effectiveGroupAllowFrom).toEqual(["+1111", "+2222"]); + expect(lists.effectiveGroupAllowFrom).toEqual(["+1111"]); }); it("resolves access + effective allowlists in one shared call", () => { @@ -89,7 +89,7 @@ describe("security/dm-policy-shared", () => { expect(resolved.decision).toBe("allow"); expect(resolved.reason).toBe("dmPolicy=pairing (allowlisted)"); expect(resolved.effectiveAllowFrom).toEqual(["owner", "paired-user"]); - expect(resolved.effectiveGroupAllowFrom).toEqual(["group:room", "paired-user"]); + expect(resolved.effectiveGroupAllowFrom).toEqual(["group:room"]); }); it("keeps allowlist mode strict in shared resolver (no pairing-store fallback)", () => {
src/security/dm-policy-shared.ts+16 −13 modified@@ -1,3 +1,4 @@ +import { mergeDmAllowFromSources, resolveGroupAllowFromSources } from "../channels/allow-from.js"; import type { ChannelId } from "../channels/plugins/types.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { normalizeStringEntries } from "../shared/string-normalization.js"; @@ -11,21 +12,23 @@ export function resolveEffectiveAllowFromLists(params: { effectiveAllowFrom: string[]; effectiveGroupAllowFrom: string[]; } { - const configAllowFrom = normalizeStringEntries( - Array.isArray(params.allowFrom) ? params.allowFrom : undefined, + const allowFrom = Array.isArray(params.allowFrom) ? params.allowFrom : undefined; + const groupAllowFrom = Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined; + const storeAllowFrom = Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined; + const effectiveAllowFrom = normalizeStringEntries( + mergeDmAllowFromSources({ + allowFrom, + storeAllowFrom, + dmPolicy: params.dmPolicy ?? undefined, + }), ); - const configGroupAllowFrom = normalizeStringEntries( - Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined, + // Group auth is explicit (groupAllowFrom fallback allowFrom). Pairing store is DM-only. + const effectiveGroupAllowFrom = normalizeStringEntries( + resolveGroupAllowFromSources({ + allowFrom, + groupAllowFrom, + }), ); - const storeAllowFrom = - params.dmPolicy === "allowlist" - ? [] - : normalizeStringEntries( - Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined, - ); - const effectiveAllowFrom = normalizeStringEntries([...configAllowFrom, ...storeAllowFrom]); - const groupBase = configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom; - const effectiveGroupAllowFrom = normalizeStringEntries([...groupBase, ...storeAllowFrom]); return { effectiveAllowFrom, effectiveGroupAllowFrom }; }
src/telegram/bot-access.ts+7 −3 modified@@ -1,4 +1,8 @@ -import { firstDefined, isSenderIdAllowed, mergeAllowFromSources } from "../channels/allow-from.js"; +import { + firstDefined, + isSenderIdAllowed, + mergeDmAllowFromSources, +} from "../channels/allow-from.js"; import type { AllowlistMatch } from "../channels/allowlist-match.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -53,11 +57,11 @@ export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAll }; }; -export const normalizeAllowFromWithStore = (params: { +export const normalizeDmAllowFromWithStore = (params: { allowFrom?: Array<string | number>; storeAllowFrom?: string[]; dmPolicy?: string; -}): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params)); +}): NormalizedAllowFrom => normalizeAllowFrom(mergeDmAllowFromSources(params)); export const isSenderAllowed = (params: { allow: NormalizedAllowFrom;
src/telegram/bot-handlers.ts+3 −3 modified@@ -28,7 +28,7 @@ import { resolveThreadSessionKeys } from "../routing/session-key.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; import { isSenderAllowed, - normalizeAllowFromWithStore, + normalizeDmAllowFromWithStore, type NormalizedAllowFrom, } from "./bot-access.js"; import type { TelegramMediaRef } from "./bot-message-context.js"; @@ -615,7 +615,7 @@ export const registerTelegramHandlers = ({ return { allowed: false, reason: "direct-disabled" }; } if (dmPolicy !== "open") { - const effectiveDmAllow = normalizeAllowFromWithStore({ + const effectiveDmAllow = normalizeDmAllowFromWithStore({ allowFrom, storeAllowFrom, dmPolicy, @@ -1273,7 +1273,7 @@ export const registerTelegramHandlers = ({ effectiveGroupAllow, hasGroupAllowOverride, } = eventAuthContext; - const effectiveDmAllow = normalizeAllowFromWithStore({ + const effectiveDmAllow = normalizeDmAllowFromWithStore({ allowFrom, storeAllowFrom, dmPolicy,
src/telegram/bot-message-context.ts+2 −2 modified@@ -40,7 +40,7 @@ import { firstDefined, isSenderAllowed, normalizeAllowFrom, - normalizeAllowFromWithStore, + normalizeDmAllowFromWithStore, } from "./bot-access.js"; import { buildGroupLabel, @@ -195,7 +195,7 @@ export const buildTelegramMessageContext = async ({ : null; const sessionKey = threadKeys?.sessionKey ?? baseSessionKey; const mentionRegexes = buildMentionRegexes(cfg, route.agentId); - const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom, dmPolicy }); + const effectiveDmAllow = normalizeDmAllowFromWithStore({ allowFrom, storeAllowFrom, dmPolicy }); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); // Group sender checks are explicit and must not inherit DM pairing-store entries. const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom);
src/telegram/bot-native-commands.ts+2 −2 modified@@ -41,7 +41,7 @@ import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; +import { isSenderAllowed, normalizeDmAllowFromWithStore } from "./bot-access.js"; import { buildCappedTelegramMenuCommands, buildPluginTelegramMenuCommands, @@ -251,7 +251,7 @@ async function resolveTelegramCommandAuth(params: { } } - const dmAllow = normalizeAllowFromWithStore({ + const dmAllow = normalizeDmAllowFromWithStore({ allowFrom: allowFrom, storeAllowFrom, dmPolicy: telegramCfg.dmPolicy ?? "pairing",
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/openclaw/openclaw/commit/051fdcc428129446e7c084260f837b7284279ce9ghsapatchWEB
- github.com/openclaw/openclaw/commit/8bdda7a651c21e98faccdbbd73081e79cffe8be0ghsapatchWEB
- github.com/advisories/GHSA-jv6r-27ww-4gw4ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-jv6r-27ww-4gw4ghsathird-party-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-32027ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-improper-authorization-via-dm-pairing-store-identity-inheritance-in-group-allowlistghsathird-party-advisoryWEB
News mentions
0No linked articles in our index yet.