refactor(security): unify dangerous name matching handling

This commit is contained in:
Peter Steinberger
2026-02-24 01:32:23 +00:00
parent 6a7c303dcc
commit 161d9841dc
17 changed files with 671 additions and 471 deletions

View File

@@ -14,12 +14,21 @@ import {
migrateLegacyConfig,
readConfigFileSnapshot,
} from "../config/config.js";
import { collectProviderDangerousNameMatchingScopes } from "../config/dangerous-name-matching.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { parseToolsBySenderTypedKey } from "../config/types.tools.js";
import {
listInterpreterLikeSafeBins,
resolveMergedSafeBinProfileFixtures,
} 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 { note } from "../terminal/note.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>;
}
function asOptionalBoolean(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}
function collectTelegramAccountScopes(
cfg: OpenClawConfig,
): Array<{ prefix: string; account: Record<string, unknown> }> {
@@ -589,148 +594,6 @@ type MutableAllowlistHit = {
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: {
hits: MutableAllowlistHit[];
pathLabel: string;
@@ -762,7 +625,7 @@ function addMutableAllowlistHits(params: {
function scanMutableAllowlistEntries(cfg: OpenClawConfig): MutableAllowlistHit[] {
const hits: MutableAllowlistHit[] = [];
for (const scope of collectProviderAccountScopes(cfg, "discord")) {
for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "discord")) {
if (scope.dangerousNameMatchingEnabled) {
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) {
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) {
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) {
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) {
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) {
continue;
}