fix(security): make allowFrom id-only by default with dangerous name opt-in (#24907)

* fix(channels): default allowFrom to id-only; add dangerous name opt-in

* docs(security): align channel allowFrom docs with id-only default
This commit is contained in:
Peter Steinberger
2026-02-24 01:01:51 +00:00
committed by GitHub
parent 41b0568b35
commit cfa44ea6b4
53 changed files with 852 additions and 100 deletions

View File

@@ -15,7 +15,7 @@ describe("slack/allow-list", () => {
expect(normalizeSlackSlug(" #Ops.Room ")).toBe("#ops.room");
});
it("matches wildcard, id, and prefixed name candidates", () => {
it("matches wildcard and id candidates by default", () => {
expect(resolveSlackAllowListMatch({ allowList: ["*"], id: "u1", name: "alice" })).toEqual({
allowed: true,
matchKey: "*",
@@ -40,6 +40,15 @@ describe("slack/allow-list", () => {
id: "u2",
name: "alice",
}),
).toEqual({ allowed: false });
expect(
resolveSlackAllowListMatch({
allowList: ["slack:alice"],
id: "u2",
name: "alice",
allowNameMatching: true,
}),
).toEqual({
allowed: true,
matchKey: "slack:alice",

View File

@@ -25,6 +25,7 @@ export function resolveSlackAllowListMatch(params: {
allowList: string[];
id?: string;
name?: string;
allowNameMatching?: boolean;
}): SlackAllowListMatch {
const allowList = params.allowList;
if (allowList.length === 0) {
@@ -40,9 +41,13 @@ export function resolveSlackAllowListMatch(params: {
{ value: id, source: "id" },
{ value: id ? `slack:${id}` : undefined, source: "prefixed-id" },
{ value: id ? `user:${id}` : undefined, source: "prefixed-user" },
{ value: name, source: "name" },
{ value: name ? `slack:${name}` : undefined, source: "prefixed-name" },
{ value: slug, source: "slug" },
...(params.allowNameMatching === true
? ([
{ value: name, source: "name" as const },
{ value: name ? `slack:${name}` : undefined, source: "prefixed-name" as const },
{ value: slug, source: "slug" as const },
] satisfies Array<{ value?: string; source: SlackAllowListMatch["matchSource"] }>)
: []),
];
for (const candidate of candidates) {
if (!candidate.value) {
@@ -59,7 +64,12 @@ export function resolveSlackAllowListMatch(params: {
return { allowed: false };
}
export function allowListMatches(params: { allowList: string[]; id?: string; name?: string }) {
export function allowListMatches(params: {
allowList: string[];
id?: string;
name?: string;
allowNameMatching?: boolean;
}) {
return resolveSlackAllowListMatch(params).allowed;
}
@@ -67,6 +77,7 @@ export function resolveSlackUserAllowed(params: {
allowList?: Array<string | number>;
userId?: string;
userName?: string;
allowNameMatching?: boolean;
}) {
const allowList = normalizeAllowListLower(params.allowList);
if (allowList.length === 0) {
@@ -76,5 +87,6 @@ export function resolveSlackUserAllowed(params: {
allowList,
id: params.userId,
name: params.userName,
allowNameMatching: params.allowNameMatching,
});
}

View File

@@ -14,14 +14,16 @@ export function isSlackSenderAllowListed(params: {
allowListLower: string[];
senderId: string;
senderName?: string;
allowNameMatching?: boolean;
}) {
const { allowListLower, senderId, senderName } = params;
const { allowListLower, senderId, senderName, allowNameMatching } = params;
return (
allowListLower.length === 0 ||
allowListMatches({
allowList: allowListLower,
id: senderId,
name: senderName,
allowNameMatching,
})
);
}

View File

@@ -47,6 +47,7 @@ export function shouldEmitSlackReactionNotification(params: {
userId: string;
userName?: string | null;
allowlist?: Array<string | number> | null;
allowNameMatching?: boolean;
}) {
const { mode, botId, messageAuthorId, userId, userName, allowlist } = params;
const effectiveMode = mode ?? "own";
@@ -68,6 +69,7 @@ export function shouldEmitSlackReactionNotification(params: {
allowList: users,
id: userId,
name: userName ?? undefined,
allowNameMatching: params.allowNameMatching,
});
}
return true;

View File

@@ -68,6 +68,7 @@ export type SlackMonitorContext = {
dmEnabled: boolean;
dmPolicy: DmPolicy;
allowFrom: string[];
allowNameMatching: boolean;
groupDmEnabled: boolean;
groupDmChannels: string[];
defaultRequireMention: boolean;
@@ -129,6 +130,7 @@ export function createSlackMonitorContext(params: {
dmEnabled: boolean;
dmPolicy: DmPolicy;
allowFrom: Array<string | number> | undefined;
allowNameMatching: boolean;
groupDmEnabled: boolean;
groupDmChannels: Array<string | number> | undefined;
defaultRequireMention?: boolean;
@@ -391,6 +393,7 @@ export function createSlackMonitorContext(params: {
dmEnabled: params.dmEnabled,
dmPolicy: params.dmPolicy,
allowFrom,
allowNameMatching: params.allowNameMatching,
groupDmEnabled: params.groupDmEnabled,
groupDmChannels,
defaultRequireMention,

View File

@@ -60,6 +60,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
dmEnabled: true,
dmPolicy: "open",
allowFrom: [],
allowNameMatching: false,
groupDmEnabled: true,
groupDmChannels: [],
defaultRequireMention: params.defaultRequireMention ?? true,

View File

@@ -142,6 +142,7 @@ export async function prepareSlackMessage(params: {
const allowMatch = resolveSlackAllowListMatch({
allowList: allowFromLower,
id: directUserId,
allowNameMatching: ctx.allowNameMatching,
});
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
if (!allowMatch.allowed) {
@@ -244,6 +245,7 @@ export async function prepareSlackMessage(params: {
allowList: channelConfig?.users,
userId: senderId,
userName: senderName,
allowNameMatching: ctx.allowNameMatching,
})
: true;
if (isRoom && !channelUserAuthorized) {
@@ -263,6 +265,7 @@ export async function prepareSlackMessage(params: {
allowList: allowFromLower,
id: senderId,
name: senderName,
allowNameMatching: ctx.allowNameMatching,
}).allowed;
const channelUsersAllowlistConfigured =
isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
@@ -272,6 +275,7 @@ export async function prepareSlackMessage(params: {
allowList: channelConfig?.users,
userId: senderId,
userName: senderName,
allowNameMatching: ctx.allowNameMatching,
})
: false;
const commandGate = resolveControlCommandGate({

View File

@@ -77,6 +77,7 @@ const baseParams = () => ({
dmEnabled: true,
dmPolicy: "open" as const,
allowFrom: [],
allowNameMatching: false,
groupDmEnabled: true,
groupDmChannels: [],
defaultRequireMention: true,

View File

@@ -210,6 +210,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
dmEnabled,
dmPolicy,
allowFrom,
allowNameMatching: slackCfg.dangerouslyAllowNameMatching === true,
groupDmEnabled,
groupDmChannels,
defaultRequireMention: slackCfg.requireMention,

View File

@@ -361,6 +361,7 @@ export async function registerSlackMonitorSlashCommands(params: {
allowList: effectiveAllowFromLower,
id: command.user_id,
name: senderName,
allowNameMatching: ctx.allowNameMatching,
});
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
if (!allowMatch.allowed) {
@@ -446,6 +447,7 @@ export async function registerSlackMonitorSlashCommands(params: {
allowList: channelConfig?.users,
userId: command.user_id,
userName: senderName,
allowNameMatching: ctx.allowNameMatching,
})
: false;
if (channelUsersAllowlistConfigured && !channelUserAllowed) {
@@ -460,6 +462,7 @@ export async function registerSlackMonitorSlashCommands(params: {
allowList: effectiveAllowFromLower,
id: command.user_id,
name: senderName,
allowNameMatching: ctx.allowNameMatching,
}).allowed;
// DMs: allow chatting in dmPolicy=open, but keep privileged command gating intact by setting
// CommandAuthorized based on allowlists/access-groups (downstream decides which commands need it).