mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:38:38 +00:00
refactor(security): unify dangerous name matching handling
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
readJsonBodyWithLimit,
|
readJsonBodyWithLimit,
|
||||||
registerWebhookTarget,
|
registerWebhookTarget,
|
||||||
rejectNonPostWebhookRequest,
|
rejectNonPostWebhookRequest,
|
||||||
|
isDangerousNameMatchingEnabled,
|
||||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||||
resolveDefaultGroupPolicy,
|
resolveDefaultGroupPolicy,
|
||||||
resolveSingleWebhookTargetAsync,
|
resolveSingleWebhookTargetAsync,
|
||||||
@@ -410,7 +411,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
const senderId = sender?.name ?? "";
|
const senderId = sender?.name ?? "";
|
||||||
const senderName = sender?.displayName ?? "";
|
const senderName = sender?.displayName ?? "";
|
||||||
const senderEmail = sender?.email ?? undefined;
|
const senderEmail = sender?.email ?? undefined;
|
||||||
const allowNameMatching = account.config.dangerouslyAllowNameMatching === true;
|
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
||||||
|
|
||||||
const allowBots = account.config.allowBots === true;
|
const allowBots = account.config.allowBots === true;
|
||||||
if (!allowBots) {
|
if (!allowBots) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createReplyPrefixOptions,
|
createReplyPrefixOptions,
|
||||||
formatTextWithAttachmentLinks,
|
formatTextWithAttachmentLinks,
|
||||||
logInboundDrop,
|
logInboundDrop,
|
||||||
|
isDangerousNameMatchingEnabled,
|
||||||
resolveControlCommandGate,
|
resolveControlCommandGate,
|
||||||
resolveOutboundMediaUrls,
|
resolveOutboundMediaUrls,
|
||||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||||
@@ -78,7 +79,7 @@ export async function handleIrcInbound(params: {
|
|||||||
const senderDisplay = message.senderHost
|
const senderDisplay = message.senderHost
|
||||||
? `${message.senderNick}!${message.senderUser ?? "?"}@${message.senderHost}`
|
? `${message.senderNick}!${message.senderUser ?? "?"}@${message.senderHost}`
|
||||||
: message.senderNick;
|
: message.senderNick;
|
||||||
const allowNameMatching = account.config.dangerouslyAllowNameMatching === true;
|
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
||||||
|
|
||||||
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
||||||
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
clearHistoryEntriesIfEnabled,
|
clearHistoryEntriesIfEnabled,
|
||||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||||
recordPendingHistoryEntryIfEnabled,
|
recordPendingHistoryEntryIfEnabled,
|
||||||
|
isDangerousNameMatchingEnabled,
|
||||||
resolveControlCommandGate,
|
resolveControlCommandGate,
|
||||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||||
resolveDefaultGroupPolicy,
|
resolveDefaultGroupPolicy,
|
||||||
@@ -212,7 +213,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
|||||||
cfg,
|
cfg,
|
||||||
accountId: opts.accountId,
|
accountId: opts.accountId,
|
||||||
});
|
});
|
||||||
const allowNameMatching = account.config.dangerouslyAllowNameMatching === true;
|
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
||||||
const botToken = opts.botToken?.trim() || account.botToken?.trim();
|
const botToken = opts.botToken?.trim() || account.botToken?.trim();
|
||||||
if (!botToken) {
|
if (!botToken) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
recordPendingHistoryEntryIfEnabled,
|
recordPendingHistoryEntryIfEnabled,
|
||||||
resolveControlCommandGate,
|
resolveControlCommandGate,
|
||||||
resolveDefaultGroupPolicy,
|
resolveDefaultGroupPolicy,
|
||||||
|
isDangerousNameMatchingEnabled,
|
||||||
resolveMentionGating,
|
resolveMentionGating,
|
||||||
formatAllowlistMatchMeta,
|
formatAllowlistMatchMeta,
|
||||||
type HistoryEntry,
|
type HistoryEntry,
|
||||||
@@ -145,7 +146,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|||||||
|
|
||||||
if (dmPolicy !== "open") {
|
if (dmPolicy !== "open") {
|
||||||
const effectiveAllowFrom = [...allowFrom.map((v) => String(v)), ...storedAllowFrom];
|
const effectiveAllowFrom = [...allowFrom.map((v) => String(v)), ...storedAllowFrom];
|
||||||
const allowNameMatching = msteamsCfg.dangerouslyAllowNameMatching === true;
|
const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
|
||||||
const allowMatch = resolveMSTeamsAllowlistMatch({
|
const allowMatch = resolveMSTeamsAllowlistMatch({
|
||||||
allowFrom: effectiveAllowFrom,
|
allowFrom: effectiveAllowFrom,
|
||||||
senderId,
|
senderId,
|
||||||
@@ -228,7 +229,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (effectiveGroupAllowFrom.length > 0) {
|
if (effectiveGroupAllowFrom.length > 0) {
|
||||||
const allowNameMatching = msteamsCfg.dangerouslyAllowNameMatching === true;
|
const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
|
||||||
const allowMatch = resolveMSTeamsAllowlistMatch({
|
const allowMatch = resolveMSTeamsAllowlistMatch({
|
||||||
allowFrom: effectiveGroupAllowFrom,
|
allowFrom: effectiveGroupAllowFrom,
|
||||||
senderId,
|
senderId,
|
||||||
@@ -252,14 +253,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|||||||
allowFrom: effectiveDmAllowFrom,
|
allowFrom: effectiveDmAllowFrom,
|
||||||
senderId,
|
senderId,
|
||||||
senderName,
|
senderName,
|
||||||
allowNameMatching: msteamsCfg?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
||||||
});
|
});
|
||||||
const groupAllowedForCommands = isMSTeamsGroupAllowed({
|
const groupAllowedForCommands = isMSTeamsGroupAllowed({
|
||||||
groupPolicy: "allowlist",
|
groupPolicy: "allowlist",
|
||||||
allowFrom: effectiveGroupAllowFrom,
|
allowFrom: effectiveGroupAllowFrom,
|
||||||
senderId,
|
senderId,
|
||||||
senderName,
|
senderName,
|
||||||
allowNameMatching: msteamsCfg?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
||||||
});
|
});
|
||||||
const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg);
|
const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg);
|
||||||
const commandGate = resolveControlCommandGate({
|
const commandGate = resolveControlCommandGate({
|
||||||
|
|||||||
@@ -14,12 +14,21 @@ import {
|
|||||||
migrateLegacyConfig,
|
migrateLegacyConfig,
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
|
import { collectProviderDangerousNameMatchingScopes } from "../config/dangerous-name-matching.js";
|
||||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||||
import { parseToolsBySenderTypedKey } from "../config/types.tools.js";
|
import { parseToolsBySenderTypedKey } from "../config/types.tools.js";
|
||||||
import {
|
import {
|
||||||
listInterpreterLikeSafeBins,
|
listInterpreterLikeSafeBins,
|
||||||
resolveMergedSafeBinProfileFixtures,
|
resolveMergedSafeBinProfileFixtures,
|
||||||
} from "../infra/exec-safe-bin-runtime-policy.js";
|
} from "../infra/exec-safe-bin-runtime-policy.js";
|
||||||
|
import {
|
||||||
|
isDiscordMutableAllowEntry,
|
||||||
|
isGoogleChatMutableAllowEntry,
|
||||||
|
isIrcMutableAllowEntry,
|
||||||
|
isMSTeamsMutableAllowEntry,
|
||||||
|
isMattermostMutableAllowEntry,
|
||||||
|
isSlackMutableAllowEntry,
|
||||||
|
} from "../security/mutable-allowlist-detectors.js";
|
||||||
import { listTelegramAccountIds, resolveTelegramAccount } from "../telegram/accounts.js";
|
import { listTelegramAccountIds, resolveTelegramAccount } from "../telegram/accounts.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
import { isRecord, resolveHomeDir } from "../utils.js";
|
import { isRecord, resolveHomeDir } from "../utils.js";
|
||||||
@@ -192,10 +201,6 @@ function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
|||||||
return value as Record<string, unknown>;
|
return value as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asOptionalBoolean(value: unknown): boolean | undefined {
|
|
||||||
return typeof value === "boolean" ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectTelegramAccountScopes(
|
function collectTelegramAccountScopes(
|
||||||
cfg: OpenClawConfig,
|
cfg: OpenClawConfig,
|
||||||
): Array<{ prefix: string; account: Record<string, unknown> }> {
|
): Array<{ prefix: string; account: Record<string, unknown> }> {
|
||||||
@@ -589,148 +594,6 @@ type MutableAllowlistHit = {
|
|||||||
dangerousFlagPath: string;
|
dangerousFlagPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProviderAccountScope = {
|
|
||||||
prefix: string;
|
|
||||||
account: Record<string, unknown>;
|
|
||||||
dangerousNameMatchingEnabled: boolean;
|
|
||||||
dangerousFlagPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function collectProviderAccountScopes(
|
|
||||||
cfg: OpenClawConfig,
|
|
||||||
provider: string,
|
|
||||||
): ProviderAccountScope[] {
|
|
||||||
const scopes: ProviderAccountScope[] = [];
|
|
||||||
const channels = asObjectRecord(cfg.channels);
|
|
||||||
if (!channels) {
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
const providerCfg = asObjectRecord(channels[provider]);
|
|
||||||
if (!providerCfg) {
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
const providerPrefix = `channels.${provider}`;
|
|
||||||
const providerDangerousFlagPath = `${providerPrefix}.dangerouslyAllowNameMatching`;
|
|
||||||
const providerDangerousNameMatchingEnabled = providerCfg.dangerouslyAllowNameMatching === true;
|
|
||||||
scopes.push({
|
|
||||||
prefix: providerPrefix,
|
|
||||||
account: providerCfg,
|
|
||||||
dangerousNameMatchingEnabled: providerDangerousNameMatchingEnabled,
|
|
||||||
dangerousFlagPath: providerDangerousFlagPath,
|
|
||||||
});
|
|
||||||
const accounts = asObjectRecord(providerCfg.accounts);
|
|
||||||
if (!accounts) {
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
for (const key of Object.keys(accounts)) {
|
|
||||||
const account = asObjectRecord(accounts[key]);
|
|
||||||
if (!account) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const accountPrefix = `${providerPrefix}.accounts.${key}`;
|
|
||||||
const accountDangerousNameMatching = asOptionalBoolean(account.dangerouslyAllowNameMatching);
|
|
||||||
scopes.push({
|
|
||||||
prefix: accountPrefix,
|
|
||||||
account,
|
|
||||||
dangerousNameMatchingEnabled:
|
|
||||||
accountDangerousNameMatching ?? providerDangerousNameMatchingEnabled,
|
|
||||||
dangerousFlagPath:
|
|
||||||
accountDangerousNameMatching == null
|
|
||||||
? providerDangerousFlagPath
|
|
||||||
: `${accountPrefix}.dangerouslyAllowNameMatching`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDiscordMutableAllowEntry(raw: string): boolean {
|
|
||||||
const text = raw.trim();
|
|
||||||
if (!text || text === "*") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const maybeMentionId = text.replace(/^<@!?/, "").replace(/>$/, "");
|
|
||||||
if (/^\d+$/.test(maybeMentionId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const prefix of ["discord:", "user:", "pk:"]) {
|
|
||||||
if (!text.startsWith(prefix)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return text.slice(prefix.length).trim().length === 0;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSlackMutableAllowEntry(raw: string): boolean {
|
|
||||||
const text = raw.trim();
|
|
||||||
if (!text || text === "*") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const mentionMatch = text.match(/^<@([A-Z0-9]+)>$/i);
|
|
||||||
if (mentionMatch && /^[A-Z0-9]{8,}$/i.test(mentionMatch[1] ?? "")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const withoutPrefix = text.replace(/^(slack|user):/i, "").trim();
|
|
||||||
if (/^[UWBCGDT][A-Z0-9]{2,}$/.test(withoutPrefix)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (/^[A-Z0-9]{8,}$/i.test(withoutPrefix)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isGoogleChatMutableAllowEntry(raw: string): boolean {
|
|
||||||
const text = raw.trim();
|
|
||||||
if (!text || text === "*") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const withoutPrefix = text.replace(/^(googlechat|google-chat|gchat):/i, "").trim();
|
|
||||||
if (!withoutPrefix) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const withoutUsers = withoutPrefix.replace(/^users\//i, "");
|
|
||||||
return withoutUsers.includes("@");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMSTeamsMutableAllowEntry(raw: string): boolean {
|
|
||||||
const text = raw.trim();
|
|
||||||
if (!text || text === "*") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const withoutPrefix = text.replace(/^(msteams|user):/i, "").trim();
|
|
||||||
return /\s/.test(withoutPrefix) || withoutPrefix.includes("@");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMattermostMutableAllowEntry(raw: string): boolean {
|
|
||||||
const text = raw.trim();
|
|
||||||
if (!text || text === "*") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const normalized = text
|
|
||||||
.replace(/^(mattermost|user):/i, "")
|
|
||||||
.replace(/^@/, "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
// Mattermost user IDs are stable 26-char lowercase/number tokens.
|
|
||||||
if (/^[a-z0-9]{26}$/.test(normalized)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIrcMutableAllowEntry(raw: string): boolean {
|
|
||||||
const text = raw.trim().toLowerCase();
|
|
||||||
if (!text || text === "*") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const normalized = text
|
|
||||||
.replace(/^irc:/, "")
|
|
||||||
.replace(/^user:/, "")
|
|
||||||
.trim();
|
|
||||||
return !normalized.includes("!") && !normalized.includes("@");
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMutableAllowlistHits(params: {
|
function addMutableAllowlistHits(params: {
|
||||||
hits: MutableAllowlistHit[];
|
hits: MutableAllowlistHit[];
|
||||||
pathLabel: string;
|
pathLabel: string;
|
||||||
@@ -762,7 +625,7 @@ function addMutableAllowlistHits(params: {
|
|||||||
function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[] {
|
function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[] {
|
||||||
const hits: MutableAllowlistHit[] = [];
|
const hits: MutableAllowlistHit[] = [];
|
||||||
|
|
||||||
for (const scope of collectProviderAccountScopes(cfg, "discord")) {
|
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "discord")) {
|
||||||
if (scope.dangerousNameMatchingEnabled) {
|
if (scope.dangerousNameMatchingEnabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -823,7 +686,7 @@ function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const scope of collectProviderAccountScopes(cfg, "slack")) {
|
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "slack")) {
|
||||||
if (scope.dangerousNameMatchingEnabled) {
|
if (scope.dangerousNameMatchingEnabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -866,7 +729,7 @@ function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const scope of collectProviderAccountScopes(cfg, "googlechat")) {
|
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "googlechat")) {
|
||||||
if (scope.dangerousNameMatchingEnabled) {
|
if (scope.dangerousNameMatchingEnabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -909,7 +772,7 @@ function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const scope of collectProviderAccountScopes(cfg, "msteams")) {
|
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "msteams")) {
|
||||||
if (scope.dangerousNameMatchingEnabled) {
|
if (scope.dangerousNameMatchingEnabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -931,7 +794,7 @@ function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[]
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const scope of collectProviderAccountScopes(cfg, "mattermost")) {
|
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "mattermost")) {
|
||||||
if (scope.dangerousNameMatchingEnabled) {
|
if (scope.dangerousNameMatchingEnabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -953,7 +816,7 @@ function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[]
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const scope of collectProviderAccountScopes(cfg, "irc")) {
|
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "irc")) {
|
||||||
if (scope.dangerousNameMatchingEnabled) {
|
if (scope.dangerousNameMatchingEnabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
84
src/config/dangerous-name-matching.ts
Normal file
84
src/config/dangerous-name-matching.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import type { OpenClawConfig } from "./config.js";
|
||||||
|
|
||||||
|
export type DangerousNameMatchingConfig = {
|
||||||
|
dangerouslyAllowNameMatching?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderDangerousNameMatchingScope = {
|
||||||
|
prefix: string;
|
||||||
|
account: Record<string, unknown>;
|
||||||
|
dangerousNameMatchingEnabled: boolean;
|
||||||
|
dangerousFlagPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asOptionalBoolean(value: unknown): boolean | undefined {
|
||||||
|
return typeof value === "boolean" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDangerousNameMatchingEnabled(
|
||||||
|
config: DangerousNameMatchingConfig | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return config?.dangerouslyAllowNameMatching === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectProviderDangerousNameMatchingScopes(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
provider: string,
|
||||||
|
): ProviderDangerousNameMatchingScope[] {
|
||||||
|
const scopes: ProviderDangerousNameMatchingScope[] = [];
|
||||||
|
const channels = asObjectRecord(cfg.channels);
|
||||||
|
if (!channels) {
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerCfg = asObjectRecord(channels[provider]);
|
||||||
|
if (!providerCfg) {
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerPrefix = `channels.${provider}`;
|
||||||
|
const providerDangerousFlagPath = `${providerPrefix}.dangerouslyAllowNameMatching`;
|
||||||
|
const providerDangerousNameMatchingEnabled = isDangerousNameMatchingEnabled(providerCfg);
|
||||||
|
|
||||||
|
scopes.push({
|
||||||
|
prefix: providerPrefix,
|
||||||
|
account: providerCfg,
|
||||||
|
dangerousNameMatchingEnabled: providerDangerousNameMatchingEnabled,
|
||||||
|
dangerousFlagPath: providerDangerousFlagPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const accounts = asObjectRecord(providerCfg.accounts);
|
||||||
|
if (!accounts) {
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(accounts)) {
|
||||||
|
const account = asObjectRecord(accounts[key]);
|
||||||
|
if (!account) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountPrefix = `${providerPrefix}.accounts.${key}`;
|
||||||
|
const accountDangerousNameMatching = asOptionalBoolean(account.dangerouslyAllowNameMatching);
|
||||||
|
|
||||||
|
scopes.push({
|
||||||
|
prefix: accountPrefix,
|
||||||
|
account,
|
||||||
|
dangerousNameMatchingEnabled:
|
||||||
|
accountDangerousNameMatching ?? providerDangerousNameMatchingEnabled,
|
||||||
|
dangerousFlagPath:
|
||||||
|
accountDangerousNameMatching == null
|
||||||
|
? providerDangerousFlagPath
|
||||||
|
: `${accountPrefix}.dangerouslyAllowNameMatching`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { createReplyReferencePlanner } from "../../auto-reply/reply/reply-refere
|
|||||||
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
|
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
|
||||||
import { recordInboundSession } from "../../channels/session.js";
|
import { recordInboundSession } from "../../channels/session.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js";
|
||||||
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
|
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
|
||||||
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
|
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
|
||||||
import type { DiscordAccountConfig } from "../../config/types.discord.js";
|
import type { DiscordAccountConfig } from "../../config/types.discord.js";
|
||||||
@@ -365,7 +366,7 @@ async function ensureAgentComponentInteractionAllowed(params: {
|
|||||||
replyOpts: params.replyOpts,
|
replyOpts: params.replyOpts,
|
||||||
componentLabel: params.componentLabel,
|
componentLabel: params.componentLabel,
|
||||||
unauthorizedReply: params.unauthorizedReply,
|
unauthorizedReply: params.unauthorizedReply,
|
||||||
allowNameMatching: params.ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
|
||||||
});
|
});
|
||||||
if (!memberAllowed) {
|
if (!memberAllowed) {
|
||||||
return null;
|
return null;
|
||||||
@@ -481,7 +482,7 @@ async function ensureDmComponentAuthorized(params: {
|
|||||||
name: user.username,
|
name: user.username,
|
||||||
tag: formatDiscordUserTag(user),
|
tag: formatDiscordUserTag(user),
|
||||||
},
|
},
|
||||||
allowNameMatching: ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
|
||||||
})
|
})
|
||||||
: { allowed: false };
|
: { allowed: false };
|
||||||
if (allowMatch.allowed) {
|
if (allowMatch.allowed) {
|
||||||
@@ -784,7 +785,7 @@ async function dispatchDiscordComponentEvent(params: {
|
|||||||
channelConfig,
|
channelConfig,
|
||||||
guildInfo,
|
guildInfo,
|
||||||
sender: { id: interactionCtx.user.id, name: interactionCtx.user.username, tag: senderTag },
|
sender: { id: interactionCtx.user.id, name: interactionCtx.user.username, tag: senderTag },
|
||||||
allowNameMatching: ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
|
||||||
});
|
});
|
||||||
const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId });
|
const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId });
|
||||||
const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
|
const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
|
||||||
@@ -982,7 +983,7 @@ async function handleDiscordComponentEvent(params: {
|
|||||||
replyOpts,
|
replyOpts,
|
||||||
componentLabel: params.componentLabel,
|
componentLabel: params.componentLabel,
|
||||||
unauthorizedReply,
|
unauthorizedReply,
|
||||||
allowNameMatching: params.ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
|
||||||
});
|
});
|
||||||
if (!memberAllowed) {
|
if (!memberAllowed) {
|
||||||
return;
|
return;
|
||||||
@@ -995,7 +996,7 @@ async function handleDiscordComponentEvent(params: {
|
|||||||
replyOpts,
|
replyOpts,
|
||||||
componentLabel: params.componentLabel,
|
componentLabel: params.componentLabel,
|
||||||
unauthorizedReply,
|
unauthorizedReply,
|
||||||
allowNameMatching: params.ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
|
||||||
});
|
});
|
||||||
if (!componentAllowed) {
|
if (!componentAllowed) {
|
||||||
return;
|
return;
|
||||||
@@ -1134,7 +1135,7 @@ async function handleDiscordModalTrigger(params: {
|
|||||||
replyOpts,
|
replyOpts,
|
||||||
componentLabel: "form",
|
componentLabel: "form",
|
||||||
unauthorizedReply,
|
unauthorizedReply,
|
||||||
allowNameMatching: params.ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
|
||||||
});
|
});
|
||||||
if (!memberAllowed) {
|
if (!memberAllowed) {
|
||||||
return;
|
return;
|
||||||
@@ -1147,7 +1148,7 @@ async function handleDiscordModalTrigger(params: {
|
|||||||
replyOpts,
|
replyOpts,
|
||||||
componentLabel: "form",
|
componentLabel: "form",
|
||||||
unauthorizedReply,
|
unauthorizedReply,
|
||||||
allowNameMatching: params.ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
|
||||||
});
|
});
|
||||||
if (!componentAllowed) {
|
if (!componentAllowed) {
|
||||||
return;
|
return;
|
||||||
@@ -1583,7 +1584,7 @@ class DiscordComponentModal extends Modal {
|
|||||||
replyOpts,
|
replyOpts,
|
||||||
componentLabel: "form",
|
componentLabel: "form",
|
||||||
unauthorizedReply: "You are not authorized to use this form.",
|
unauthorizedReply: "You are not authorized to use this form.",
|
||||||
allowNameMatching: this.ctx.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(this.ctx.discordConfig),
|
||||||
});
|
});
|
||||||
if (!memberAllowed) {
|
if (!memberAllowed) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { resolveControlCommandGate } from "../../channels/command-gating.js";
|
|||||||
import { logInboundDrop } from "../../channels/logging.js";
|
import { logInboundDrop } from "../../channels/logging.js";
|
||||||
import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js";
|
import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js";
|
||||||
import { logVerbose, shouldLogVerbose } from "../../globals.js";
|
import { logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||||
import { recordChannelActivity } from "../../infra/channel-activity.js";
|
import { recordChannelActivity } from "../../infra/channel-activity.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
@@ -190,7 +191,7 @@ export async function preflightDiscordMessage(
|
|||||||
name: sender.name,
|
name: sender.name,
|
||||||
tag: sender.tag,
|
tag: sender.tag,
|
||||||
},
|
},
|
||||||
allowNameMatching: params.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig),
|
||||||
})
|
})
|
||||||
: { allowed: false };
|
: { allowed: false };
|
||||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||||
@@ -564,7 +565,7 @@ export async function preflightDiscordMessage(
|
|||||||
guildInfo,
|
guildInfo,
|
||||||
memberRoleIds,
|
memberRoleIds,
|
||||||
sender,
|
sender,
|
||||||
allowNameMatching: params.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isDirectMessage) {
|
if (!isDirectMessage) {
|
||||||
@@ -581,7 +582,7 @@ export async function preflightDiscordMessage(
|
|||||||
name: sender.name,
|
name: sender.name,
|
||||||
tag: sender.tag,
|
tag: sender.tag,
|
||||||
},
|
},
|
||||||
{ allowNameMatching: params.discordConfig?.dangerouslyAllowNameMatching === true },
|
{ allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig) },
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type StatusReactionAdapter,
|
type StatusReactionAdapter,
|
||||||
} from "../../channels/status-reactions.js";
|
} from "../../channels/status-reactions.js";
|
||||||
import { createTypingCallbacks } from "../../channels/typing.js";
|
import { createTypingCallbacks } from "../../channels/typing.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js";
|
||||||
import { resolveDiscordPreviewStreamMode } from "../../config/discord-preview-streaming.js";
|
import { resolveDiscordPreviewStreamMode } from "../../config/discord-preview-streaming.js";
|
||||||
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
|
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
|
||||||
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
|
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
|
||||||
@@ -199,7 +200,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
channelConfig,
|
channelConfig,
|
||||||
guildInfo,
|
guildInfo,
|
||||||
sender: { id: sender.id, name: sender.name, tag: sender.tag },
|
sender: { id: sender.id, name: sender.name, tag: sender.tag },
|
||||||
allowNameMatching: discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(discordConfig),
|
||||||
});
|
});
|
||||||
const storePath = resolveStorePath(cfg.session?.store, {
|
const storePath = resolveStorePath(cfg.session?.store, {
|
||||||
agentId: route.agentId,
|
agentId: route.agentId,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
|
|||||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||||
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
|
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
|
||||||
import type { OpenClawConfig, loadConfig } from "../../config/config.js";
|
import type { OpenClawConfig, loadConfig } from "../../config/config.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js";
|
||||||
import { resolveOpenProviderRuntimeGroupPolicy } from "../../config/runtime-group-policy.js";
|
import { resolveOpenProviderRuntimeGroupPolicy } from "../../config/runtime-group-policy.js";
|
||||||
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
|
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
|
||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
@@ -1283,7 +1284,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
name: sender.name,
|
name: sender.name,
|
||||||
tag: sender.tag,
|
tag: sender.tag,
|
||||||
},
|
},
|
||||||
{ allowNameMatching: discordConfig?.dangerouslyAllowNameMatching === true },
|
{ allowNameMatching: isDangerousNameMatchingEnabled(discordConfig) },
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
const guildInfo = resolveDiscordGuildEntry({
|
const guildInfo = resolveDiscordGuildEntry({
|
||||||
@@ -1374,7 +1375,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
name: sender.name,
|
name: sender.name,
|
||||||
tag: sender.tag,
|
tag: sender.tag,
|
||||||
},
|
},
|
||||||
{ allowNameMatching: discordConfig?.dangerouslyAllowNameMatching === true },
|
{ allowNameMatching: isDangerousNameMatchingEnabled(discordConfig) },
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
if (!permitted) {
|
if (!permitted) {
|
||||||
@@ -1412,7 +1413,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
guildInfo,
|
guildInfo,
|
||||||
memberRoleIds,
|
memberRoleIds,
|
||||||
sender,
|
sender,
|
||||||
allowNameMatching: discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(discordConfig),
|
||||||
});
|
});
|
||||||
const authorizers = useAccessGroups
|
const authorizers = useAccessGroups
|
||||||
? [
|
? [
|
||||||
@@ -1518,7 +1519,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
channelConfig,
|
channelConfig,
|
||||||
guildInfo,
|
guildInfo,
|
||||||
sender: { id: sender.id, name: sender.name, tag: sender.tag },
|
sender: { id: sender.id, name: sender.name, tag: sender.tag },
|
||||||
allowNameMatching: discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(discordConfig),
|
||||||
});
|
});
|
||||||
const ctxPayload = finalizeInboundContext({
|
const ctxPayload = finalizeInboundContext({
|
||||||
Body: prompt,
|
Body: prompt,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "../../config/commands.js";
|
} from "../../config/commands.js";
|
||||||
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
|
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js";
|
||||||
import {
|
import {
|
||||||
GROUP_POLICY_BLOCKED_LABEL,
|
GROUP_POLICY_BLOCKED_LABEL,
|
||||||
resolveOpenProviderRuntimeGroupPolicy,
|
resolveOpenProviderRuntimeGroupPolicy,
|
||||||
@@ -559,7 +560,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
runtime,
|
runtime,
|
||||||
botUserId,
|
botUserId,
|
||||||
allowNameMatching: discordCfg.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(discordCfg),
|
||||||
guildEntries,
|
guildEntries,
|
||||||
logger,
|
logger,
|
||||||
}),
|
}),
|
||||||
@@ -571,7 +572,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
runtime,
|
runtime,
|
||||||
botUserId,
|
botUserId,
|
||||||
allowNameMatching: discordCfg.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(discordCfg),
|
||||||
guildEntries,
|
guildEntries,
|
||||||
logger,
|
logger,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "discord-api-types/v10";
|
} from "discord-api-types/v10";
|
||||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js";
|
||||||
import type { DiscordAccountConfig } from "../../config/types.js";
|
import type { DiscordAccountConfig } from "../../config/types.js";
|
||||||
import {
|
import {
|
||||||
allowListMatches,
|
allowListMatches,
|
||||||
@@ -156,7 +157,7 @@ async function authorizeVoiceCommand(
|
|||||||
guildInfo,
|
guildInfo,
|
||||||
memberRoleIds,
|
memberRoleIds,
|
||||||
sender,
|
sender,
|
||||||
allowNameMatching: params.discordConfig?.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ownerAllowList = normalizeDiscordAllowList(
|
const ownerAllowList = normalizeDiscordAllowList(
|
||||||
@@ -171,7 +172,7 @@ async function authorizeVoiceCommand(
|
|||||||
name: sender.name,
|
name: sender.name,
|
||||||
tag: sender.tag,
|
tag: sender.tag,
|
||||||
},
|
},
|
||||||
{ allowNameMatching: params.discordConfig?.dangerouslyAllowNameMatching === true },
|
{ allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig) },
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
|||||||
export type { OpenClawConfig } from "../config/config.js";
|
export type { OpenClawConfig } from "../config/config.js";
|
||||||
/** @deprecated Use OpenClawConfig instead */
|
/** @deprecated Use OpenClawConfig instead */
|
||||||
export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js";
|
export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js";
|
||||||
|
export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js";
|
||||||
|
|
||||||
export type { FileLockHandle, FileLockOptions } from "./file-lock.js";
|
export type { FileLockHandle, FileLockOptions } from "./file-lock.js";
|
||||||
export { acquireFileLock, withFileLock } from "./file-lock.js";
|
export { acquireFileLock, withFileLock } from "./file-lock.js";
|
||||||
|
|||||||
@@ -8,36 +8,17 @@ import {
|
|||||||
import { formatCliCommand } from "../cli/command-format.js";
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../config/commands.js";
|
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../config/commands.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js";
|
||||||
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||||
import type { SecurityAuditFinding, SecurityAuditSeverity } from "./audit.js";
|
import type { SecurityAuditFinding, SecurityAuditSeverity } from "./audit.js";
|
||||||
import { resolveDmAllowState } from "./dm-policy-shared.js";
|
import { resolveDmAllowState } from "./dm-policy-shared.js";
|
||||||
|
import { isDiscordMutableAllowEntry } from "./mutable-allowlist-detectors.js";
|
||||||
|
|
||||||
function normalizeAllowFromList(list: Array<string | number> | undefined | null): string[] {
|
function normalizeAllowFromList(list: Array<string | number> | undefined | null): string[] {
|
||||||
return normalizeStringEntries(Array.isArray(list) ? list : undefined);
|
return normalizeStringEntries(Array.isArray(list) ? list : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DISCORD_ALLOWLIST_ID_PREFIXES = ["discord:", "user:", "pk:"] as const;
|
|
||||||
|
|
||||||
function isDiscordNameBasedAllowEntry(raw: string | number): boolean {
|
|
||||||
const text = String(raw).trim();
|
|
||||||
if (!text || text === "*") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const maybeId = text.replace(/^<@!?/, "").replace(/>$/, "");
|
|
||||||
if (/^\d+$/.test(maybeId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const prefixed = DISCORD_ALLOWLIST_ID_PREFIXES.find((prefix) => text.startsWith(prefix));
|
|
||||||
if (prefixed) {
|
|
||||||
const candidate = text.slice(prefixed.length);
|
|
||||||
if (candidate) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDiscordNameBasedEntries(params: {
|
function addDiscordNameBasedEntries(params: {
|
||||||
target: Set<string>;
|
target: Set<string>;
|
||||||
values: unknown;
|
values: unknown;
|
||||||
@@ -47,7 +28,7 @@ function addDiscordNameBasedEntries(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const value of params.values) {
|
for (const value of params.values) {
|
||||||
if (!isDiscordNameBasedAllowEntry(value as string | number)) {
|
if (!isDiscordMutableAllowEntry(String(value))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const text = String(value).trim();
|
const text = String(value).trim();
|
||||||
@@ -76,6 +57,42 @@ function classifyChannelWarningSeverity(message: string): SecurityAuditSeverity
|
|||||||
return "warn";
|
return "warn";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dedupeFindings(findings: SecurityAuditFinding[]): SecurityAuditFinding[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: SecurityAuditFinding[] = [];
|
||||||
|
for (const finding of findings) {
|
||||||
|
const key = [
|
||||||
|
finding.checkId,
|
||||||
|
finding.severity,
|
||||||
|
finding.title,
|
||||||
|
finding.detail ?? "",
|
||||||
|
finding.remediation ?? "",
|
||||||
|
].join("\n");
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
out.push(finding);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExplicitProviderAccountConfig(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
provider: string,
|
||||||
|
accountId: string,
|
||||||
|
): boolean {
|
||||||
|
const channel = cfg.channels?.[provider];
|
||||||
|
if (!channel || typeof channel !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const accounts = (channel as { accounts?: Record<string, unknown> }).accounts;
|
||||||
|
if (!accounts || typeof accounts !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return accountId in accounts;
|
||||||
|
}
|
||||||
|
|
||||||
export async function collectChannelSecurityFindings(params: {
|
export async function collectChannelSecurityFindings(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
plugins: ReturnType<typeof listChannelPlugins>;
|
plugins: ReturnType<typeof listChannelPlugins>;
|
||||||
@@ -166,299 +183,317 @@ export async function collectChannelSecurityFindings(params: {
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
accountIds,
|
accountIds,
|
||||||
});
|
});
|
||||||
const account = plugin.config.resolveAccount(params.cfg, defaultAccountId);
|
const orderedAccountIds = Array.from(new Set([defaultAccountId, ...accountIds]));
|
||||||
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, params.cfg) : true;
|
|
||||||
if (!enabled) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const configured = plugin.config.isConfigured
|
|
||||||
? await plugin.config.isConfigured(account, params.cfg)
|
|
||||||
: true;
|
|
||||||
if (!configured) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountConfig = (account as { config?: Record<string, unknown> } | null | undefined)
|
for (const accountId of orderedAccountIds) {
|
||||||
?.config;
|
const hasExplicitAccountPath = hasExplicitProviderAccountConfig(
|
||||||
if (accountConfig?.dangerouslyAllowNameMatching === true) {
|
params.cfg,
|
||||||
findings.push({
|
plugin.id,
|
||||||
checkId: `channels.${plugin.id}.allowFrom.dangerous_name_matching_enabled`,
|
accountId,
|
||||||
severity: "info",
|
);
|
||||||
title: `${plugin.meta.label ?? plugin.id} dangerous name matching is enabled`,
|
const account = plugin.config.resolveAccount(params.cfg, accountId);
|
||||||
detail:
|
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, params.cfg) : true;
|
||||||
"dangerouslyAllowNameMatching=true re-enables mutable name/email/tag matching for sender authorization. This is a break-glass compatibility mode, not a hardened default.",
|
if (!enabled) {
|
||||||
remediation:
|
continue;
|
||||||
"Prefer stable sender IDs in allowlists, then disable dangerouslyAllowNameMatching.",
|
}
|
||||||
});
|
const configured = plugin.config.isConfigured
|
||||||
}
|
? await plugin.config.isConfigured(account, params.cfg)
|
||||||
|
: true;
|
||||||
|
if (!configured) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (plugin.id === "discord") {
|
const accountConfig = (account as { config?: Record<string, unknown> } | null | undefined)
|
||||||
const discordCfg =
|
?.config;
|
||||||
(account as { config?: Record<string, unknown> } | null)?.config ??
|
if (isDangerousNameMatchingEnabled(accountConfig)) {
|
||||||
({} as Record<string, unknown>);
|
const accountNote =
|
||||||
const dangerousNameMatchingEnabled = discordCfg.dangerouslyAllowNameMatching === true;
|
orderedAccountIds.length > 1 || hasExplicitAccountPath ? ` (account: ${accountId})` : "";
|
||||||
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
|
findings.push({
|
||||||
const discordNameBasedAllowEntries = new Set<string>();
|
checkId: `channels.${plugin.id}.allowFrom.dangerous_name_matching_enabled`,
|
||||||
addDiscordNameBasedEntries({
|
severity: "info",
|
||||||
target: discordNameBasedAllowEntries,
|
title: `${plugin.meta.label ?? plugin.id} dangerous name matching is enabled${accountNote}`,
|
||||||
values: discordCfg.allowFrom,
|
detail:
|
||||||
source: "channels.discord.allowFrom",
|
"dangerouslyAllowNameMatching=true re-enables mutable name/email/tag matching for sender authorization. This is a break-glass compatibility mode, not a hardened default.",
|
||||||
});
|
remediation:
|
||||||
addDiscordNameBasedEntries({
|
"Prefer stable sender IDs in allowlists, then disable dangerouslyAllowNameMatching.",
|
||||||
target: discordNameBasedAllowEntries,
|
});
|
||||||
values: (discordCfg.dm as { allowFrom?: unknown } | undefined)?.allowFrom,
|
}
|
||||||
source: "channels.discord.dm.allowFrom",
|
|
||||||
});
|
if (plugin.id === "discord") {
|
||||||
addDiscordNameBasedEntries({
|
const discordCfg =
|
||||||
target: discordNameBasedAllowEntries,
|
(account as { config?: Record<string, unknown> } | null)?.config ??
|
||||||
values: storeAllowFrom,
|
({} as Record<string, unknown>);
|
||||||
source: "~/.openclaw/credentials/discord-allowFrom.json",
|
const dangerousNameMatchingEnabled = isDangerousNameMatchingEnabled(discordCfg);
|
||||||
});
|
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
|
||||||
const discordGuildEntries = (discordCfg.guilds as Record<string, unknown> | undefined) ?? {};
|
const discordNameBasedAllowEntries = new Set<string>();
|
||||||
for (const [guildKey, guildValue] of Object.entries(discordGuildEntries)) {
|
const discordPathPrefix =
|
||||||
if (!guildValue || typeof guildValue !== "object") {
|
orderedAccountIds.length > 1 || hasExplicitAccountPath
|
||||||
continue;
|
? `channels.discord.accounts.${accountId}`
|
||||||
}
|
: "channels.discord";
|
||||||
const guild = guildValue as Record<string, unknown>;
|
|
||||||
addDiscordNameBasedEntries({
|
addDiscordNameBasedEntries({
|
||||||
target: discordNameBasedAllowEntries,
|
target: discordNameBasedAllowEntries,
|
||||||
values: guild.users,
|
values: discordCfg.allowFrom,
|
||||||
source: `channels.discord.guilds.${guildKey}.users`,
|
source: `${discordPathPrefix}.allowFrom`,
|
||||||
});
|
});
|
||||||
const channels = guild.channels;
|
addDiscordNameBasedEntries({
|
||||||
if (!channels || typeof channels !== "object") {
|
target: discordNameBasedAllowEntries,
|
||||||
continue;
|
values: (discordCfg.dm as { allowFrom?: unknown } | undefined)?.allowFrom,
|
||||||
}
|
source: `${discordPathPrefix}.dm.allowFrom`,
|
||||||
for (const [channelKey, channelValue] of Object.entries(
|
});
|
||||||
channels as Record<string, unknown>,
|
addDiscordNameBasedEntries({
|
||||||
)) {
|
target: discordNameBasedAllowEntries,
|
||||||
if (!channelValue || typeof channelValue !== "object") {
|
values: storeAllowFrom,
|
||||||
|
source: "~/.openclaw/credentials/discord-allowFrom.json",
|
||||||
|
});
|
||||||
|
const discordGuildEntries =
|
||||||
|
(discordCfg.guilds as Record<string, unknown> | undefined) ?? {};
|
||||||
|
for (const [guildKey, guildValue] of Object.entries(discordGuildEntries)) {
|
||||||
|
if (!guildValue || typeof guildValue !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const channel = channelValue as Record<string, unknown>;
|
const guild = guildValue as Record<string, unknown>;
|
||||||
addDiscordNameBasedEntries({
|
addDiscordNameBasedEntries({
|
||||||
target: discordNameBasedAllowEntries,
|
target: discordNameBasedAllowEntries,
|
||||||
values: channel.users,
|
values: guild.users,
|
||||||
source: `channels.discord.guilds.${guildKey}.channels.${channelKey}.users`,
|
source: `${discordPathPrefix}.guilds.${guildKey}.users`,
|
||||||
});
|
});
|
||||||
}
|
const channels = guild.channels;
|
||||||
}
|
|
||||||
if (discordNameBasedAllowEntries.size > 0) {
|
|
||||||
const examples = Array.from(discordNameBasedAllowEntries).slice(0, 5);
|
|
||||||
const more =
|
|
||||||
discordNameBasedAllowEntries.size > examples.length
|
|
||||||
? ` (+${discordNameBasedAllowEntries.size - examples.length} more)`
|
|
||||||
: "";
|
|
||||||
findings.push({
|
|
||||||
checkId: "channels.discord.allowFrom.name_based_entries",
|
|
||||||
severity: dangerousNameMatchingEnabled ? "info" : "warn",
|
|
||||||
title: dangerousNameMatchingEnabled
|
|
||||||
? "Discord allowlist uses break-glass name/tag matching"
|
|
||||||
: "Discord allowlist contains name or tag entries",
|
|
||||||
detail: dangerousNameMatchingEnabled
|
|
||||||
? "Discord name/tag allowlist matching is explicitly enabled via dangerouslyAllowNameMatching. This mutable-identity mode is operator-selected break-glass behavior and out-of-scope for vulnerability reports by itself. " +
|
|
||||||
`Found: ${examples.join(", ")}${more}.`
|
|
||||||
: "Discord name/tag allowlist matching uses normalized slugs and can collide across users. " +
|
|
||||||
`Found: ${examples.join(", ")}${more}.`,
|
|
||||||
remediation: dangerousNameMatchingEnabled
|
|
||||||
? "Prefer stable Discord IDs (or <@id>/user:<id>/pk:<id>), then disable dangerouslyAllowNameMatching."
|
|
||||||
: "Prefer stable Discord IDs (or <@id>/user:<id>/pk:<id>) in channels.discord.allowFrom and channels.discord.guilds.*.users, or explicitly opt in with dangerouslyAllowNameMatching=true if you accept the risk.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const nativeEnabled = resolveNativeCommandsEnabled({
|
|
||||||
providerId: "discord",
|
|
||||||
providerSetting: coerceNativeSetting(
|
|
||||||
(discordCfg.commands as { native?: unknown } | undefined)?.native,
|
|
||||||
),
|
|
||||||
globalSetting: params.cfg.commands?.native,
|
|
||||||
});
|
|
||||||
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
|
|
||||||
providerId: "discord",
|
|
||||||
providerSetting: coerceNativeSetting(
|
|
||||||
(discordCfg.commands as { nativeSkills?: unknown } | undefined)?.nativeSkills,
|
|
||||||
),
|
|
||||||
globalSetting: params.cfg.commands?.nativeSkills,
|
|
||||||
});
|
|
||||||
const slashEnabled = nativeEnabled || nativeSkillsEnabled;
|
|
||||||
if (slashEnabled) {
|
|
||||||
const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
|
|
||||||
const groupPolicy =
|
|
||||||
(discordCfg.groupPolicy as string | undefined) ?? defaultGroupPolicy ?? "allowlist";
|
|
||||||
const guildEntries = discordGuildEntries;
|
|
||||||
const guildsConfigured = Object.keys(guildEntries).length > 0;
|
|
||||||
const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => {
|
|
||||||
if (!guild || typeof guild !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const g = guild as Record<string, unknown>;
|
|
||||||
if (Array.isArray(g.users) && g.users.length > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const channels = g.channels;
|
|
||||||
if (!channels || typeof channels !== "object") {
|
if (!channels || typeof channels !== "object") {
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
return Object.values(channels as Record<string, unknown>).some((channel) => {
|
for (const [channelKey, channelValue] of Object.entries(
|
||||||
if (!channel || typeof channel !== "object") {
|
channels as Record<string, unknown>,
|
||||||
return false;
|
)) {
|
||||||
|
if (!channelValue || typeof channelValue !== "object") {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
const c = channel as Record<string, unknown>;
|
const channel = channelValue as Record<string, unknown>;
|
||||||
return Array.isArray(c.users) && c.users.length > 0;
|
addDiscordNameBasedEntries({
|
||||||
});
|
target: discordNameBasedAllowEntries,
|
||||||
});
|
values: channel.users,
|
||||||
const dmAllowFromRaw = (discordCfg.dm as { allowFrom?: unknown } | undefined)?.allowFrom;
|
source: `${discordPathPrefix}.guilds.${guildKey}.channels.${channelKey}.users`,
|
||||||
const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
|
});
|
||||||
const ownerAllowFromConfigured =
|
}
|
||||||
normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
|
}
|
||||||
|
if (discordNameBasedAllowEntries.size > 0) {
|
||||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
const examples = Array.from(discordNameBasedAllowEntries).slice(0, 5);
|
||||||
if (
|
const more =
|
||||||
!useAccessGroups &&
|
discordNameBasedAllowEntries.size > examples.length
|
||||||
groupPolicy !== "disabled" &&
|
? ` (+${discordNameBasedAllowEntries.size - examples.length} more)`
|
||||||
guildsConfigured &&
|
: "";
|
||||||
!hasAnyUserAllowlist
|
|
||||||
) {
|
|
||||||
findings.push({
|
findings.push({
|
||||||
checkId: "channels.discord.commands.native.unrestricted",
|
checkId: "channels.discord.allowFrom.name_based_entries",
|
||||||
severity: "critical",
|
severity: dangerousNameMatchingEnabled ? "info" : "warn",
|
||||||
title: "Discord slash commands are unrestricted",
|
title: dangerousNameMatchingEnabled
|
||||||
detail:
|
? "Discord allowlist uses break-glass name/tag matching"
|
||||||
"commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.",
|
: "Discord allowlist contains name or tag entries",
|
||||||
remediation:
|
detail: dangerousNameMatchingEnabled
|
||||||
"Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users).",
|
? "Discord name/tag allowlist matching is explicitly enabled via dangerouslyAllowNameMatching. This mutable-identity mode is operator-selected break-glass behavior and out-of-scope for vulnerability reports by itself. " +
|
||||||
});
|
`Found: ${examples.join(", ")}${more}.`
|
||||||
} else if (
|
: "Discord name/tag allowlist matching uses normalized slugs and can collide across users. " +
|
||||||
useAccessGroups &&
|
`Found: ${examples.join(", ")}${more}.`,
|
||||||
groupPolicy !== "disabled" &&
|
remediation: dangerousNameMatchingEnabled
|
||||||
guildsConfigured &&
|
? "Prefer stable Discord IDs (or <@id>/user:<id>/pk:<id>), then disable dangerouslyAllowNameMatching."
|
||||||
!ownerAllowFromConfigured &&
|
: "Prefer stable Discord IDs (or <@id>/user:<id>/pk:<id>) in channels.discord.allowFrom and channels.discord.guilds.*.users, or explicitly opt in with dangerouslyAllowNameMatching=true if you accept the risk.",
|
||||||
!hasAnyUserAllowlist
|
|
||||||
) {
|
|
||||||
findings.push({
|
|
||||||
checkId: "channels.discord.commands.native.no_allowlists",
|
|
||||||
severity: "warn",
|
|
||||||
title: "Discord slash commands have no allowlists",
|
|
||||||
detail:
|
|
||||||
"Discord slash commands are enabled, but neither an owner allowFrom list nor any per-guild/channel users allowlist is configured; /… commands will be rejected for everyone.",
|
|
||||||
remediation:
|
|
||||||
"Add your user id to channels.discord.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users.",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
const nativeEnabled = resolveNativeCommandsEnabled({
|
||||||
}
|
providerId: "discord",
|
||||||
|
providerSetting: coerceNativeSetting(
|
||||||
if (plugin.id === "slack") {
|
(discordCfg.commands as { native?: unknown } | undefined)?.native,
|
||||||
const slackCfg =
|
),
|
||||||
(account as { config?: Record<string, unknown>; dm?: Record<string, unknown> } | null)
|
globalSetting: params.cfg.commands?.native,
|
||||||
?.config ?? ({} as Record<string, unknown>);
|
});
|
||||||
const nativeEnabled = resolveNativeCommandsEnabled({
|
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
|
||||||
providerId: "slack",
|
providerId: "discord",
|
||||||
providerSetting: coerceNativeSetting(
|
providerSetting: coerceNativeSetting(
|
||||||
(slackCfg.commands as { native?: unknown } | undefined)?.native,
|
(discordCfg.commands as { nativeSkills?: unknown } | undefined)?.nativeSkills,
|
||||||
),
|
),
|
||||||
globalSetting: params.cfg.commands?.native,
|
globalSetting: params.cfg.commands?.nativeSkills,
|
||||||
});
|
});
|
||||||
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
|
const slashEnabled = nativeEnabled || nativeSkillsEnabled;
|
||||||
providerId: "slack",
|
if (slashEnabled) {
|
||||||
providerSetting: coerceNativeSetting(
|
const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
|
||||||
(slackCfg.commands as { nativeSkills?: unknown } | undefined)?.nativeSkills,
|
const groupPolicy =
|
||||||
),
|
(discordCfg.groupPolicy as string | undefined) ?? defaultGroupPolicy ?? "allowlist";
|
||||||
globalSetting: params.cfg.commands?.nativeSkills,
|
const guildEntries = discordGuildEntries;
|
||||||
});
|
const guildsConfigured = Object.keys(guildEntries).length > 0;
|
||||||
const slashCommandEnabled =
|
const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => {
|
||||||
nativeEnabled ||
|
if (!guild || typeof guild !== "object") {
|
||||||
nativeSkillsEnabled ||
|
|
||||||
(slackCfg.slashCommand as { enabled?: unknown } | undefined)?.enabled === true;
|
|
||||||
if (slashCommandEnabled) {
|
|
||||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
|
||||||
if (!useAccessGroups) {
|
|
||||||
findings.push({
|
|
||||||
checkId: "channels.slack.commands.slash.useAccessGroups_off",
|
|
||||||
severity: "critical",
|
|
||||||
title: "Slack slash commands bypass access groups",
|
|
||||||
detail:
|
|
||||||
"Slack slash/native commands are enabled while commands.useAccessGroups=false; this can allow unrestricted /… command execution from channels/users you didn't explicitly authorize.",
|
|
||||||
remediation: "Set commands.useAccessGroups=true (recommended).",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const allowFromRaw = (
|
|
||||||
account as
|
|
||||||
| { config?: { allowFrom?: unknown }; dm?: { allowFrom?: unknown } }
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
)?.config?.allowFrom;
|
|
||||||
const legacyAllowFromRaw = (
|
|
||||||
account as { dm?: { allowFrom?: unknown } } | null | undefined
|
|
||||||
)?.dm?.allowFrom;
|
|
||||||
const allowFrom = Array.isArray(allowFromRaw)
|
|
||||||
? allowFromRaw
|
|
||||||
: Array.isArray(legacyAllowFromRaw)
|
|
||||||
? legacyAllowFromRaw
|
|
||||||
: [];
|
|
||||||
const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []);
|
|
||||||
const ownerAllowFromConfigured =
|
|
||||||
normalizeAllowFromList([...allowFrom, ...storeAllowFrom]).length > 0;
|
|
||||||
const channels = (slackCfg.channels as Record<string, unknown> | undefined) ?? {};
|
|
||||||
const hasAnyChannelUsersAllowlist = Object.values(channels).some((value) => {
|
|
||||||
if (!value || typeof value !== "object") {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const channel = value as Record<string, unknown>;
|
const g = guild as Record<string, unknown>;
|
||||||
return Array.isArray(channel.users) && channel.users.length > 0;
|
if (Array.isArray(g.users) && g.users.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const channels = g.channels;
|
||||||
|
if (!channels || typeof channels !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object.values(channels as Record<string, unknown>).some((channel) => {
|
||||||
|
if (!channel || typeof channel !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const c = channel as Record<string, unknown>;
|
||||||
|
return Array.isArray(c.users) && c.users.length > 0;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (!ownerAllowFromConfigured && !hasAnyChannelUsersAllowlist) {
|
const dmAllowFromRaw = (discordCfg.dm as { allowFrom?: unknown } | undefined)?.allowFrom;
|
||||||
|
const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
|
||||||
|
const ownerAllowFromConfigured =
|
||||||
|
normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
|
||||||
|
|
||||||
|
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||||
|
if (
|
||||||
|
!useAccessGroups &&
|
||||||
|
groupPolicy !== "disabled" &&
|
||||||
|
guildsConfigured &&
|
||||||
|
!hasAnyUserAllowlist
|
||||||
|
) {
|
||||||
findings.push({
|
findings.push({
|
||||||
checkId: "channels.slack.commands.slash.no_allowlists",
|
checkId: "channels.discord.commands.native.unrestricted",
|
||||||
severity: "warn",
|
severity: "critical",
|
||||||
title: "Slack slash commands have no allowlists",
|
title: "Discord slash commands are unrestricted",
|
||||||
detail:
|
detail:
|
||||||
"Slack slash/native commands are enabled, but neither an owner allowFrom list nor any channels.<id>.users allowlist is configured; /… commands will be rejected for everyone.",
|
"commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.",
|
||||||
remediation:
|
remediation:
|
||||||
"Approve yourself via pairing (recommended), or set channels.slack.allowFrom and/or channels.slack.channels.<id>.users.",
|
"Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users).",
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
useAccessGroups &&
|
||||||
|
groupPolicy !== "disabled" &&
|
||||||
|
guildsConfigured &&
|
||||||
|
!ownerAllowFromConfigured &&
|
||||||
|
!hasAnyUserAllowlist
|
||||||
|
) {
|
||||||
|
findings.push({
|
||||||
|
checkId: "channels.discord.commands.native.no_allowlists",
|
||||||
|
severity: "warn",
|
||||||
|
title: "Discord slash commands have no allowlists",
|
||||||
|
detail:
|
||||||
|
"Discord slash commands are enabled, but neither an owner allowFrom list nor any per-guild/channel users allowlist is configured; /… commands will be rejected for everyone.",
|
||||||
|
remediation:
|
||||||
|
"Add your user id to channels.discord.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const dmPolicy = plugin.security.resolveDmPolicy?.({
|
if (plugin.id === "slack") {
|
||||||
cfg: params.cfg,
|
const slackCfg =
|
||||||
accountId: defaultAccountId,
|
(account as { config?: Record<string, unknown>; dm?: Record<string, unknown> } | null)
|
||||||
account,
|
?.config ?? ({} as Record<string, unknown>);
|
||||||
});
|
const nativeEnabled = resolveNativeCommandsEnabled({
|
||||||
if (dmPolicy) {
|
providerId: "slack",
|
||||||
await warnDmPolicy({
|
providerSetting: coerceNativeSetting(
|
||||||
label: plugin.meta.label ?? plugin.id,
|
(slackCfg.commands as { native?: unknown } | undefined)?.native,
|
||||||
provider: plugin.id,
|
),
|
||||||
dmPolicy: dmPolicy.policy,
|
globalSetting: params.cfg.commands?.native,
|
||||||
allowFrom: dmPolicy.allowFrom,
|
});
|
||||||
policyPath: dmPolicy.policyPath,
|
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
|
||||||
allowFromPath: dmPolicy.allowFromPath,
|
providerId: "slack",
|
||||||
normalizeEntry: dmPolicy.normalizeEntry,
|
providerSetting: coerceNativeSetting(
|
||||||
});
|
(slackCfg.commands as { nativeSkills?: unknown } | undefined)?.nativeSkills,
|
||||||
}
|
),
|
||||||
|
globalSetting: params.cfg.commands?.nativeSkills,
|
||||||
|
});
|
||||||
|
const slashCommandEnabled =
|
||||||
|
nativeEnabled ||
|
||||||
|
nativeSkillsEnabled ||
|
||||||
|
(slackCfg.slashCommand as { enabled?: unknown } | undefined)?.enabled === true;
|
||||||
|
if (slashCommandEnabled) {
|
||||||
|
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||||
|
if (!useAccessGroups) {
|
||||||
|
findings.push({
|
||||||
|
checkId: "channels.slack.commands.slash.useAccessGroups_off",
|
||||||
|
severity: "critical",
|
||||||
|
title: "Slack slash commands bypass access groups",
|
||||||
|
detail:
|
||||||
|
"Slack slash/native commands are enabled while commands.useAccessGroups=false; this can allow unrestricted /… command execution from channels/users you didn't explicitly authorize.",
|
||||||
|
remediation: "Set commands.useAccessGroups=true (recommended).",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const allowFromRaw = (
|
||||||
|
account as
|
||||||
|
| { config?: { allowFrom?: unknown }; dm?: { allowFrom?: unknown } }
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
)?.config?.allowFrom;
|
||||||
|
const legacyAllowFromRaw = (
|
||||||
|
account as { dm?: { allowFrom?: unknown } } | null | undefined
|
||||||
|
)?.dm?.allowFrom;
|
||||||
|
const allowFrom = Array.isArray(allowFromRaw)
|
||||||
|
? allowFromRaw
|
||||||
|
: Array.isArray(legacyAllowFromRaw)
|
||||||
|
? legacyAllowFromRaw
|
||||||
|
: [];
|
||||||
|
const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []);
|
||||||
|
const ownerAllowFromConfigured =
|
||||||
|
normalizeAllowFromList([...allowFrom, ...storeAllowFrom]).length > 0;
|
||||||
|
const channels = (slackCfg.channels as Record<string, unknown> | undefined) ?? {};
|
||||||
|
const hasAnyChannelUsersAllowlist = Object.values(channels).some((value) => {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const channel = value as Record<string, unknown>;
|
||||||
|
return Array.isArray(channel.users) && channel.users.length > 0;
|
||||||
|
});
|
||||||
|
if (!ownerAllowFromConfigured && !hasAnyChannelUsersAllowlist) {
|
||||||
|
findings.push({
|
||||||
|
checkId: "channels.slack.commands.slash.no_allowlists",
|
||||||
|
severity: "warn",
|
||||||
|
title: "Slack slash commands have no allowlists",
|
||||||
|
detail:
|
||||||
|
"Slack slash/native commands are enabled, but neither an owner allowFrom list nor any channels.<id>.users allowlist is configured; /… commands will be rejected for everyone.",
|
||||||
|
remediation:
|
||||||
|
"Approve yourself via pairing (recommended), or set channels.slack.allowFrom and/or channels.slack.channels.<id>.users.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (plugin.security.collectWarnings) {
|
const dmPolicy = plugin.security.resolveDmPolicy?.({
|
||||||
const warnings = await plugin.security.collectWarnings({
|
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
accountId: defaultAccountId,
|
accountId,
|
||||||
account,
|
account,
|
||||||
});
|
});
|
||||||
for (const message of warnings ?? []) {
|
if (dmPolicy) {
|
||||||
const trimmed = String(message).trim();
|
await warnDmPolicy({
|
||||||
if (!trimmed) {
|
label: plugin.meta.label ?? plugin.id,
|
||||||
continue;
|
provider: plugin.id,
|
||||||
}
|
dmPolicy: dmPolicy.policy,
|
||||||
findings.push({
|
allowFrom: dmPolicy.allowFrom,
|
||||||
checkId: `channels.${plugin.id}.warning.${findings.length + 1}`,
|
policyPath: dmPolicy.policyPath,
|
||||||
severity: classifyChannelWarningSeverity(trimmed),
|
allowFromPath: dmPolicy.allowFromPath,
|
||||||
title: `${plugin.meta.label ?? plugin.id} security warning`,
|
normalizeEntry: dmPolicy.normalizeEntry,
|
||||||
detail: trimmed.replace(/^-\s*/, ""),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.id === "telegram") {
|
if (plugin.security.collectWarnings) {
|
||||||
|
const warnings = await plugin.security.collectWarnings({
|
||||||
|
cfg: params.cfg,
|
||||||
|
accountId,
|
||||||
|
account,
|
||||||
|
});
|
||||||
|
for (const message of warnings ?? []) {
|
||||||
|
const trimmed = String(message).trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
findings.push({
|
||||||
|
checkId: `channels.${plugin.id}.warning.${findings.length + 1}`,
|
||||||
|
severity: classifyChannelWarningSeverity(trimmed),
|
||||||
|
title: `${plugin.meta.label ?? plugin.id} security warning`,
|
||||||
|
detail: trimmed.replace(/^-\s*/, ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.id !== "telegram") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const allowTextCommands = params.cfg.commands?.text !== false;
|
const allowTextCommands = params.cfg.commands?.text !== false;
|
||||||
if (!allowTextCommands) {
|
if (!allowTextCommands) {
|
||||||
continue;
|
continue;
|
||||||
@@ -614,5 +649,5 @@ export async function collectChannelSecurityFindings(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return findings;
|
return dedupeFindings(findings);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const isWindows = process.platform === "win32";
|
|||||||
function stubChannelPlugin(params: {
|
function stubChannelPlugin(params: {
|
||||||
id: "discord" | "slack" | "telegram";
|
id: "discord" | "slack" | "telegram";
|
||||||
label: string;
|
label: string;
|
||||||
resolveAccount: (cfg: OpenClawConfig) => unknown;
|
resolveAccount: (cfg: OpenClawConfig, accountId: string | null | undefined) => unknown;
|
||||||
|
listAccountIds?: (cfg: OpenClawConfig) => string[];
|
||||||
}): ChannelPlugin {
|
}): ChannelPlugin {
|
||||||
return {
|
return {
|
||||||
id: params.id,
|
id: params.id,
|
||||||
@@ -31,11 +32,15 @@ function stubChannelPlugin(params: {
|
|||||||
},
|
},
|
||||||
security: {},
|
security: {},
|
||||||
config: {
|
config: {
|
||||||
listAccountIds: (cfg) => {
|
listAccountIds:
|
||||||
const enabled = Boolean((cfg.channels as Record<string, unknown> | undefined)?.[params.id]);
|
params.listAccountIds ??
|
||||||
return enabled ? ["default"] : [];
|
((cfg) => {
|
||||||
},
|
const enabled = Boolean(
|
||||||
resolveAccount: (cfg) => params.resolveAccount(cfg),
|
(cfg.channels as Record<string, unknown> | undefined)?.[params.id],
|
||||||
|
);
|
||||||
|
return enabled ? ["default"] : [];
|
||||||
|
}),
|
||||||
|
resolveAccount: (cfg, accountId) => params.resolveAccount(cfg, accountId),
|
||||||
isEnabled: () => true,
|
isEnabled: () => true,
|
||||||
isConfigured: () => true,
|
isConfigured: () => true,
|
||||||
},
|
},
|
||||||
@@ -45,19 +50,46 @@ function stubChannelPlugin(params: {
|
|||||||
const discordPlugin = stubChannelPlugin({
|
const discordPlugin = stubChannelPlugin({
|
||||||
id: "discord",
|
id: "discord",
|
||||||
label: "Discord",
|
label: "Discord",
|
||||||
resolveAccount: (cfg) => ({ config: cfg.channels?.discord ?? {} }),
|
listAccountIds: (cfg) => {
|
||||||
|
const ids = Object.keys(cfg.channels?.discord?.accounts ?? {});
|
||||||
|
return ids.length > 0 ? ids : ["default"];
|
||||||
|
},
|
||||||
|
resolveAccount: (cfg, accountId) => {
|
||||||
|
const resolvedAccountId = typeof accountId === "string" && accountId ? accountId : "default";
|
||||||
|
const base = cfg.channels?.discord ?? {};
|
||||||
|
const account = cfg.channels?.discord?.accounts?.[resolvedAccountId] ?? {};
|
||||||
|
return { config: { ...base, ...account } };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const slackPlugin = stubChannelPlugin({
|
const slackPlugin = stubChannelPlugin({
|
||||||
id: "slack",
|
id: "slack",
|
||||||
label: "Slack",
|
label: "Slack",
|
||||||
resolveAccount: (cfg) => ({ config: cfg.channels?.slack ?? {} }),
|
listAccountIds: (cfg) => {
|
||||||
|
const ids = Object.keys(cfg.channels?.slack?.accounts ?? {});
|
||||||
|
return ids.length > 0 ? ids : ["default"];
|
||||||
|
},
|
||||||
|
resolveAccount: (cfg, accountId) => {
|
||||||
|
const resolvedAccountId = typeof accountId === "string" && accountId ? accountId : "default";
|
||||||
|
const base = cfg.channels?.slack ?? {};
|
||||||
|
const account = cfg.channels?.slack?.accounts?.[resolvedAccountId] ?? {};
|
||||||
|
return { config: { ...base, ...account } };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const telegramPlugin = stubChannelPlugin({
|
const telegramPlugin = stubChannelPlugin({
|
||||||
id: "telegram",
|
id: "telegram",
|
||||||
label: "Telegram",
|
label: "Telegram",
|
||||||
resolveAccount: (cfg) => ({ config: cfg.channels?.telegram ?? {} }),
|
listAccountIds: (cfg) => {
|
||||||
|
const ids = Object.keys(cfg.channels?.telegram?.accounts ?? {});
|
||||||
|
return ids.length > 0 ? ids : ["default"];
|
||||||
|
},
|
||||||
|
resolveAccount: (cfg, accountId) => {
|
||||||
|
const resolvedAccountId = typeof accountId === "string" && accountId ? accountId : "default";
|
||||||
|
const base = cfg.channels?.telegram ?? {};
|
||||||
|
const account = cfg.channels?.telegram?.accounts?.[resolvedAccountId] ?? {};
|
||||||
|
return { config: { ...base, ...account } };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function successfulProbeResult(url: string) {
|
function successfulProbeResult(url: string) {
|
||||||
@@ -1537,6 +1569,79 @@ describe("security audit", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("audits non-default Discord accounts for dangerous name matching", async () => {
|
||||||
|
await withChannelSecurityStateDir(async () => {
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
enabled: true,
|
||||||
|
token: "t",
|
||||||
|
accounts: {
|
||||||
|
alpha: { token: "a" },
|
||||||
|
beta: {
|
||||||
|
token: "b",
|
||||||
|
dangerouslyAllowNameMatching: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await runSecurityAudit({
|
||||||
|
config: cfg,
|
||||||
|
includeFilesystem: false,
|
||||||
|
includeChannelSecurity: true,
|
||||||
|
plugins: [discordPlugin],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.findings).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
checkId: "channels.discord.allowFrom.dangerous_name_matching_enabled",
|
||||||
|
title: expect.stringContaining("(account: beta)"),
|
||||||
|
severity: "info",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("audits name-based allowlists on non-default Discord accounts", async () => {
|
||||||
|
await withChannelSecurityStateDir(async () => {
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
enabled: true,
|
||||||
|
token: "t",
|
||||||
|
accounts: {
|
||||||
|
alpha: {
|
||||||
|
token: "a",
|
||||||
|
allowFrom: ["123456789012345678"],
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
token: "b",
|
||||||
|
allowFrom: ["Alice#1234"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await runSecurityAudit({
|
||||||
|
config: cfg,
|
||||||
|
includeFilesystem: false,
|
||||||
|
includeChannelSecurity: true,
|
||||||
|
plugins: [discordPlugin],
|
||||||
|
});
|
||||||
|
|
||||||
|
const finding = res.findings.find(
|
||||||
|
(entry) => entry.checkId === "channels.discord.allowFrom.name_based_entries",
|
||||||
|
);
|
||||||
|
expect(finding).toBeDefined();
|
||||||
|
expect(finding?.detail).toContain("channels.discord.accounts.beta.allowFrom:Alice#1234");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("does not warn when Discord allowlists use ID-style entries only", async () => {
|
it("does not warn when Discord allowlists use ID-style entries only", async () => {
|
||||||
await withChannelSecurityStateDir(async () => {
|
await withChannelSecurityStateDir(async () => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
|
|||||||
101
src/security/mutable-allowlist-detectors.ts
Normal file
101
src/security/mutable-allowlist-detectors.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
export function isDiscordMutableAllowEntry(raw: string): boolean {
|
||||||
|
const text = raw.trim();
|
||||||
|
if (!text || text === "*") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeMentionId = text.replace(/^<@!?/, "").replace(/>$/, "");
|
||||||
|
if (/^\d+$/.test(maybeMentionId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prefix of ["discord:", "user:", "pk:"]) {
|
||||||
|
if (!text.startsWith(prefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return text.slice(prefix.length).trim().length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSlackMutableAllowEntry(raw: string): boolean {
|
||||||
|
const text = raw.trim();
|
||||||
|
if (!text || text === "*") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mentionMatch = text.match(/^<@([A-Z0-9]+)>$/i);
|
||||||
|
if (mentionMatch && /^[A-Z0-9]{8,}$/i.test(mentionMatch[1] ?? "")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutPrefix = text.replace(/^(slack|user):/i, "").trim();
|
||||||
|
if (/^[UWBCGDT][A-Z0-9]{2,}$/.test(withoutPrefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^[A-Z0-9]{8,}$/i.test(withoutPrefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGoogleChatMutableAllowEntry(raw: string): boolean {
|
||||||
|
const text = raw.trim();
|
||||||
|
if (!text || text === "*") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutPrefix = text.replace(/^(googlechat|google-chat|gchat):/i, "").trim();
|
||||||
|
if (!withoutPrefix) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutUsers = withoutPrefix.replace(/^users\//i, "");
|
||||||
|
return withoutUsers.includes("@");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMSTeamsMutableAllowEntry(raw: string): boolean {
|
||||||
|
const text = raw.trim();
|
||||||
|
if (!text || text === "*") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutPrefix = text.replace(/^(msteams|user):/i, "").trim();
|
||||||
|
return /\s/.test(withoutPrefix) || withoutPrefix.includes("@");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMattermostMutableAllowEntry(raw: string): boolean {
|
||||||
|
const text = raw.trim();
|
||||||
|
if (!text || text === "*") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = text
|
||||||
|
.replace(/^(mattermost|user):/i, "")
|
||||||
|
.replace(/^@/, "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
// Mattermost user IDs are stable 26-char lowercase/number tokens.
|
||||||
|
if (/^[a-z0-9]{26}$/.test(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIrcMutableAllowEntry(raw: string): boolean {
|
||||||
|
const text = raw.trim().toLowerCase();
|
||||||
|
if (!text || text === "*") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = text
|
||||||
|
.replace(/^irc:/, "")
|
||||||
|
.replace(/^user:/, "")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return !normalized.includes("!") && !normalized.includes("@");
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
summarizeMapping,
|
summarizeMapping,
|
||||||
} from "../../channels/allowlists/resolve-utils.js";
|
} from "../../channels/allowlists/resolve-utils.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js";
|
||||||
import {
|
import {
|
||||||
resolveOpenProviderRuntimeGroupPolicy,
|
resolveOpenProviderRuntimeGroupPolicy,
|
||||||
resolveDefaultGroupPolicy,
|
resolveDefaultGroupPolicy,
|
||||||
@@ -210,7 +211,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
dmEnabled,
|
dmEnabled,
|
||||||
dmPolicy,
|
dmPolicy,
|
||||||
allowFrom,
|
allowFrom,
|
||||||
allowNameMatching: slackCfg.dangerouslyAllowNameMatching === true,
|
allowNameMatching: isDangerousNameMatchingEnabled(slackCfg),
|
||||||
groupDmEnabled,
|
groupDmEnabled,
|
||||||
groupDmChannels,
|
groupDmChannels,
|
||||||
defaultRequireMention: slackCfg.requireMention,
|
defaultRequireMention: slackCfg.requireMention,
|
||||||
|
|||||||
Reference in New Issue
Block a user