fix(security): keep DM pairing allowlists out of group auth

This commit is contained in:
Peter Steinberger
2026-02-26 12:58:06 +01:00
parent d08dafb08f
commit 8bdda7a651
15 changed files with 194 additions and 54 deletions

View File

@@ -1,4 +1,8 @@
import { firstDefined, isSenderIdAllowed, mergeAllowFromSources } from "../channels/allow-from.js";
import {
firstDefined,
isSenderIdAllowed,
mergeDmAllowFromSources,
} from "../channels/allow-from.js";
export type NormalizedAllowFrom = {
entries: string[];
@@ -27,11 +31,11 @@ export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAll
};
};
export const normalizeAllowFromWithStore = (params: {
export const normalizeDmAllowFromWithStore = (params: {
allowFrom?: Array<string | number>;
storeAllowFrom?: string[];
dmPolicy?: string;
}): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params));
}): NormalizedAllowFrom => normalizeAllowFrom(mergeDmAllowFromSources(params));
export const isSenderAllowed = (params: {
allow: NormalizedAllowFrom;

View File

@@ -182,6 +182,41 @@ describe("handleLineWebhookEvents", () => {
expect(processMessage).toHaveBeenCalledTimes(1);
});
it("blocks group sender that is only present in pairing-store allowlist", async () => {
const processMessage = vi.fn();
readAllowFromStoreMock.mockResolvedValueOnce(["user-paired"]);
const event = {
type: "message",
message: { id: "m3b", type: "text", text: "hi" },
replyToken: "reply-token",
timestamp: Date.now(),
source: { type: "group", groupId: "group-1", userId: "user-paired" },
mode: "active",
webhookEventId: "evt-3b",
deliveryContext: { isRedelivery: false },
} as MessageEvent;
await handleLineWebhookEvents([event], {
cfg: {
channels: { line: { groupPolicy: "allowlist", groupAllowFrom: ["user-owner"] } },
},
account: {
accountId: "default",
enabled: true,
channelAccessToken: "token",
channelSecret: "secret",
tokenSource: "config",
config: { groupPolicy: "allowlist", groupAllowFrom: ["user-owner"] },
},
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
});
expect(buildLineMessageContextMock).not.toHaveBeenCalled();
expect(processMessage).not.toHaveBeenCalled();
});
it("blocks group messages when wildcard group config disables groups", async () => {
const processMessage = vi.fn();
const event = {

View File

@@ -21,7 +21,12 @@ import {
upsertChannelPairingRequest,
} from "../pairing/pairing-store.js";
import type { RuntimeEnv } from "../runtime.js";
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
import {
firstDefined,
isSenderAllowed,
normalizeAllowFrom,
normalizeDmAllowFromWithStore,
} from "./bot-access.js";
import {
getLineSourceInfo,
buildLineMessageContext,
@@ -117,7 +122,7 @@ async function shouldProcessLineEvent(
const dmPolicy = account.config.dmPolicy ?? "pairing";
const storeAllowFrom = await readChannelAllowFromStore("line").catch(() => []);
const effectiveDmAllow = normalizeAllowFromWithStore({
const effectiveDmAllow = normalizeDmAllowFromWithStore({
allowFrom: account.config.allowFrom,
storeAllowFrom,
dmPolicy,
@@ -132,11 +137,9 @@ async function shouldProcessLineEvent(
account.config.groupAllowFrom,
fallbackGroupAllowFrom,
);
const effectiveGroupAllow = normalizeAllowFromWithStore({
allowFrom: groupAllowFrom,
storeAllowFrom,
dmPolicy,
});
// Group authorization stays explicit to group allowlists and must not
// inherit DM pairing-store identities.
const effectiveGroupAllow = normalizeAllowFrom(groupAllowFrom);
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
const { groupPolicy, providerMissingFallbackApplied } =
resolveAllowlistProviderRuntimeGroupPolicy({