fix(whatsapp): enforce exact raw JID domains

This commit is contained in:
joshavant
2026-08-12 02:36:40 -05:00
parent 75288cd4dd
commit 689b44f105
8 changed files with 51 additions and 19 deletions
@@ -185,6 +185,12 @@ describe("whatsapp inbound context visibility", () => {
["whatsapp:777@lid", "777@lid"],
["whatsapp:whatsapp:777@hosted.lid", "777@hosted.lid"],
[" 777@s.whatsapp.net ", "+777"],
["777@S.WHATSAPP.NET", "+777"],
["777@HOSTED", "+777"],
["777@LID", "777@lid"],
["777@HOSTED.LID", "777@hosted.lid"],
["123-456@G.US", "123-456@g.us"],
["777@NEWSLETTER", "777@newsletter"],
])("keeps raw native quote participant %j out of comparable facts", (participant, allowFrom) => {
const context = describeReplyContext({
extendedTextMessage: {
@@ -246,16 +246,21 @@ describe("isBotMentionedFromTargets", () => {
});
it.each([
"whatsapp:216372600647751@lid",
"whatsapp:whatsapp:216372600647751@lid",
" 216372600647751@lid ",
])("does not treat raw native mention %j as a self mention", (mentionedJid) => {
["whatsapp:216372600647751@lid", "216372600647751@lid"],
["whatsapp:whatsapp:216372600647751@lid", "216372600647751@lid"],
[" 216372600647751@lid ", "216372600647751@lid"],
["216372600647751@LID", "216372600647751@lid"],
["15551234567@S.WHATSAPP.NET", "15551234567@s.whatsapp.net"],
["216372600647751@LID", "216372600647751@LID"],
["123-456@G.US", "123-456@G.US"],
["216372600647751@NEWSLETTER", "216372600647751@NEWSLETTER"],
])("does not treat raw native mention %j as self identity %j", (mentionedJid, selfJid) => {
const msg = makeMsg({
body: "hey",
mentionedJids: [mentionedJid],
selfE164: "+15551234567",
selfJid: "15551234567@s.whatsapp.net",
selfLid: "216372600647751@lid",
selfJid,
selfLid: selfJid.includes("@lid") || selfJid.includes("@LID") ? selfJid : undefined,
});
expectMentioned(msg, mentionCfg, false);
+15
View File
@@ -1,6 +1,7 @@
// Whatsapp identity tests cover strict provider identity aliases.
import { describe, expect, it } from "vitest";
import { normalizeWhatsAppLidJid, resolveComparableIdentity } from "./identity.js";
import { parseExactWhatsAppJid, parseWhatsAppJid } from "./phone-input.js";
describe("normalizeWhatsAppLidJid", () => {
it.each([
@@ -66,4 +67,18 @@ describe("normalizeWhatsAppLidJid", () => {
"123@hosted.lid",
);
});
it.each([
["123@S.WHATSAPP.NET", "123@s.whatsapp.net"],
["123:4@HOSTED", "123:4@hosted"],
["123@LID", "123@lid"],
["123:4@HOSTED.LID", "123:4@hosted.lid"],
["123-456@G.US", "123-456@g.us"],
["123@NEWSLETTER", "123@newsletter"],
])("keeps raw domain case exact while convenience-normalizing %j", (raw, canonical) => {
expect(parseExactWhatsAppJid(raw)).toBeNull();
expect(parseWhatsAppJid(raw)?.jid).toBe(canonical);
const resolved = resolveComparableIdentity({ jid: raw });
expect(resolved).toEqual({ jid: raw, lid: null, e164: null });
});
});
+3 -1
View File
@@ -109,7 +109,9 @@ export function getComparableIdentityValues(
identity: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
): string[] {
const resolved = resolveComparableIdentity(identity);
return [resolved.e164, resolved.jid, resolved.lid].filter((value): value is string =>
const parsedJid = resolved.jid ? parseExactWhatsAppJid(resolved.jid) : null;
const comparableJid = parsedJid?.kind === "pn" || parsedJid?.kind === "lid" ? resolved.jid : null;
return [resolved.e164, comparableJid, resolved.lid].filter((value): value is string =>
Boolean(value),
);
}
+1 -1
View File
@@ -210,7 +210,7 @@ export async function resolveWhatsAppCommandAuthorized(params: {
});
const isGroup = admission.conversation.kind === "group";
const sender = getSenderIdentity(params.msg, params.authDir);
const stableSender = isGroup ? (sender.e164 ?? sender.lid ?? sender.jid) : admission.sender.id;
const stableSender = isGroup ? (sender.e164 ?? sender.lid) : admission.sender.id;
if (!stableSender) {
return false;
}
@@ -242,7 +242,7 @@ vi.mock("./session.js", async () => {
groupFetchAllParticipating: vi.fn().mockResolvedValue({}),
updateMediaMessage: vi.fn(),
logger: {},
user: { id: "me@s.whatsapp.net" },
user: { id: "15550001111@s.whatsapp.net" },
};
return currentMockSocket;
}),
@@ -442,7 +442,7 @@ describe("web inbound media saves with extension", () => {
contextInfo: {
stanzaId: "quoted-image",
participant: "222@s.whatsapp.net",
mentionedJid: ["me@s.whatsapp.net"],
mentionedJid: ["15550001111@s.whatsapp.net"],
quotedMessage: {
imageMessage: { mimetype: "image/jpeg" },
},
@@ -490,7 +490,7 @@ describe("web inbound media saves with extension", () => {
text: "what is in your image?",
contextInfo: {
stanzaId: "bot-image",
participant: "me@s.whatsapp.net",
participant: "15550001111@s.whatsapp.net",
quotedMessage: { imageMessage: { mimetype: "image/jpeg" } },
},
},
@@ -510,7 +510,7 @@ describe("web inbound media saves with extension", () => {
const retry = encryptMediaRetryRequest(
quoted!.key as never,
Buffer.alloc(32, 1),
"me@s.whatsapp.net",
"15550001111@s.whatsapp.net",
);
const retryNode = Array.isArray(retry.content)
? retry.content.find((node) => node.tag === "rmr")
@@ -151,6 +151,10 @@ describe("typed WhatsApp direct identity access", () => {
["123:4@hosted.lid", "123@hosted.lid", true],
["15550001111:4@s.whatsapp.net", "+15550001111", true],
["15550001111:4@hosted", "+15550001111", true],
["123@LID", "123@lid", false],
["123:4@HOSTED.LID", "123@hosted.lid", false],
["15550001111@S.WHATSAPP.NET", "+15550001111", false],
["15550001111:4@HOSTED", "+15550001111", false],
])(
"authorizes group command sender %j against exact direct entry %j as %j",
async (senderJid, allowFrom, expected) => {
+7 -7
View File
@@ -1,11 +1,11 @@
// Whatsapp helper module owns dependency-free strict phone input parsing.
const WHATSAPP_PHONE_INPUT_RE = /^\+?[\d ().-]+$/;
const WHATSAPP_JID_PATTERNS = [
["pn", /^(\d+)(?::(\d+))?@(s\.whatsapp\.net|hosted)$/i],
["pn", /^(\d+)()@(c\.us)$/i],
["lid", /^(\d+)(?::(\d+))?@(lid|hosted\.lid)$/i],
["group", /^([0-9]+(?:-[0-9]+)*)()@(g\.us)$/i],
["newsletter", /^([0-9]+)()@(newsletter)$/i],
["pn", /^(\d+)(?::(\d+))?@(s\.whatsapp\.net|hosted)$/],
["pn", /^(\d+)()@(c\.us)$/],
["lid", /^(\d+)(?::(\d+))?@(lid|hosted\.lid)$/],
["group", /^([0-9]+(?:-[0-9]+)*)()@(g\.us)$/],
["newsletter", /^([0-9]+)()@(newsletter)$/],
] as const;
type ParsedWhatsAppJid = {
@@ -43,7 +43,7 @@ export function parseExactWhatsAppJid(value: string): ParsedWhatsAppJid | null {
for (const [kind, pattern] of WHATSAPP_JID_PATTERNS) {
const match = value.match(pattern);
const digits = match?.[1];
const domain = match?.[3]?.toLowerCase();
const domain = match?.[3];
if (!digits || !domain) {
continue;
}
@@ -57,7 +57,7 @@ export function parseWhatsAppJid(value: string): ParsedWhatsAppJid | null {
const stripped = stripWhatsAppTargetPrefixes(value);
const hasGroupPrefix = /^group:/i.test(stripped);
const candidate = trimWhatsAppAsciiSpaces(stripped.replace(/^group:/i, ""));
const parsed = parseExactWhatsAppJid(candidate);
const parsed = parseExactWhatsAppJid(candidate.toLowerCase());
return hasGroupPrefix && parsed?.kind !== "group" ? null : parsed;
}