fix(security): lock down inbound DMs by default

This commit is contained in:
Peter Steinberger
2026-01-06 17:51:38 +01:00
parent 327ad3c9c7
commit 967cef80bc
36 changed files with 2093 additions and 203 deletions

View File

@@ -16,6 +16,10 @@ import {
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
import { mediaKindFromMime } from "../media/constants.js";
import {
readProviderAllowFromStore,
upsertProviderPairingRequest,
} from "../pairing/pairing-store.js";
import type { RuntimeEnv } from "../runtime.js";
import { createIMessageRpcClient } from "./client.js";
import { sendMessageIMessage } from "./send.js";
@@ -130,6 +134,7 @@ export async function monitorIMessageProvider(
const allowFrom = resolveAllowFrom(opts);
const groupAllowFrom = resolveGroupAllowFrom(opts);
const groupPolicy = cfg.imessage?.groupPolicy ?? "open";
const dmPolicy = cfg.imessage?.dmPolicy ?? "pairing";
const mentionRegexes = buildMentionRegexes(cfg);
const includeAttachments =
opts.includeAttachments ?? cfg.imessage?.includeAttachments ?? false;
@@ -153,20 +158,34 @@ export async function monitorIMessageProvider(
if (isGroup && !chatId) return;
const groupId = isGroup ? String(chatId) : undefined;
const storeAllowFrom = await readProviderAllowFromStore("imessage").catch(
() => [],
);
const effectiveDmAllowFrom = Array.from(
new Set([...allowFrom, ...storeAllowFrom]),
)
.map((v) => String(v).trim())
.filter(Boolean);
const effectiveGroupAllowFrom = Array.from(
new Set([...groupAllowFrom, ...storeAllowFrom]),
)
.map((v) => String(v).trim())
.filter(Boolean);
if (isGroup) {
if (groupPolicy === "disabled") {
logVerbose("Blocked iMessage group message (groupPolicy: disabled)");
return;
}
if (groupPolicy === "allowlist") {
if (groupAllowFrom.length === 0) {
if (effectiveGroupAllowFrom.length === 0) {
logVerbose(
"Blocked iMessage group message (groupPolicy: allowlist, no groupAllowFrom)",
);
return;
}
const allowed = isAllowedIMessageSender({
allowFrom: groupAllowFrom,
allowFrom: effectiveGroupAllowFrom,
sender,
chatId: chatId ?? undefined,
chatGuid,
@@ -192,16 +211,64 @@ export async function monitorIMessageProvider(
}
}
const dmAuthorized = isAllowedIMessageSender({
allowFrom,
sender,
chatId: chatId ?? undefined,
chatGuid,
chatIdentifier,
});
if (!isGroup && !dmAuthorized) {
logVerbose(`Blocked iMessage sender ${sender} (not in allowFrom)`);
return;
const dmHasWildcard = effectiveDmAllowFrom.includes("*");
const dmAuthorized =
dmPolicy === "open"
? true
: dmHasWildcard ||
(effectiveDmAllowFrom.length > 0 &&
isAllowedIMessageSender({
allowFrom: effectiveDmAllowFrom,
sender,
chatId: chatId ?? undefined,
chatGuid,
chatIdentifier,
}));
if (!isGroup) {
if (dmPolicy === "disabled") return;
if (!dmAuthorized) {
if (dmPolicy === "pairing") {
const senderId = normalizeIMessageHandle(sender);
const { code } = await upsertProviderPairingRequest({
provider: "imessage",
id: senderId,
meta: {
sender: senderId,
chatId: chatId ? String(chatId) : undefined,
},
});
logVerbose(
`imessage pairing request sender=${senderId} code=${code}`,
);
try {
await sendMessageIMessage(
sender,
[
"Clawdbot: access not configured.",
"",
`Pairing code: ${code}`,
"",
"Ask the bot owner to approve with:",
"clawdbot pairing approve --provider imessage <code>",
].join("\n"),
{
client,
maxBytes: mediaMaxBytes,
...(chatId ? { chatId } : {}),
},
);
} catch (err) {
logVerbose(
`imessage pairing reply failed for ${senderId}: ${String(err)}`,
);
}
} else {
logVerbose(
`Blocked iMessage sender ${sender} (dmPolicy=${dmPolicy})`,
);
}
return;
}
}
const messageText = (message.text ?? "").trim();
@@ -217,9 +284,9 @@ export async function monitorIMessageProvider(
});
const canDetectMention = mentionRegexes.length > 0;
const commandAuthorized = isGroup
? groupAllowFrom.length > 0
? effectiveGroupAllowFrom.length > 0
? isAllowedIMessageSender({
allowFrom: groupAllowFrom,
allowFrom: effectiveGroupAllowFrom,
sender,
chatId: chatId ?? undefined,
chatGuid,