From 22a783efe07c245c8ee9e706b69e2aebd373fb74 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:43:16 +0100 Subject: [PATCH] fix: check an invitation's sender also when it claims one of the user's own addresses --- lib/__tests__/plugin-host-calendar-respond.test.ts | 4 ++++ lib/plugin-sandbox/assistant/c.calendar.ts | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/__tests__/plugin-host-calendar-respond.test.ts b/lib/__tests__/plugin-host-calendar-respond.test.ts index 43e486328..b87e3263c 100644 --- a/lib/__tests__/plugin-host-calendar-respond.test.ts +++ b/lib/__tests__/plugin-host-calendar-respond.test.ts @@ -79,6 +79,10 @@ describe('api.calendar.respond', () => { vi.mocked(client.getEmail).mockResolvedValueOnce({ ...invitation, from: [{ email: 'bob@example.org', name: 'Bob' }] } as never); parsed.participants = { p1: { ...alice, email: 'bob@example.org' } } as never; await expect(call(ASSISTANT, 'calendar.respond', 'inv', 'accepted')).rejects.toThrow(/organises/); + // From forged as the user's own address, for someone else's event, not verified. + vi.mocked(client.getEmail).mockResolvedValueOnce({ ...invitation, from: [{ email: 'bob@example.org', name: 'Bob' }] } as never); + parsed.participants = { p1: alice, p2: bob } as never; + await expect(call(ASSISTANT, 'calendar.respond', 'inv', 'accepted')).rejects.toThrow(/may not come from/); // Someone else's event whose sender doesn't match its organiser. vi.mocked(client.getEmail).mockResolvedValueOnce({ ...invitation, from: [{ email: 'mallory@evil.example', name: 'Alice' }] } as never); parsed.participants = { p1: alice, p2: bob } as never; diff --git a/lib/plugin-sandbox/assistant/c.calendar.ts b/lib/plugin-sandbox/assistant/c.calendar.ts index 4e51d7ffa..4fb58a8f0 100644 --- a/lib/plugin-sandbox/assistant/c.calendar.ts +++ b/lib/plugin-sandbox/assistant/c.calendar.ts @@ -76,11 +76,10 @@ export async function doCalendarRespond(emailIdArg: unknown, statusArg: unknown) if (itip !== 'request') throw new Error(`${method}: the message is not an invitation that asks for an answer`); const emails = identityEmails(); - const sender = email.from?.[0]?.email?.toLowerCase(); - if (!(sender && emails.includes(sender))) { - const trust = getInvitationTrustAssessment(parsedEvent, email, itip, emails); - if (trust?.level === 'warning') throw new Error(`${method}: the invitation may not come from who it names; answer it in the reader`); - } + // Always checked, also when From is one of the user's own addresses (it + // can be forged): only a sender the server verified passes a mismatch. + const trust = getInvitationTrustAssessment(parsedEvent, email, itip, emails); + if (trust?.level === 'warning') throw new Error(`${method}: the invitation may not come from who it names; answer it in the reader`); const existing = await ownCopy(client, parsedEvent.uid); const { participant: myParticipant, isOrganizer } = attendeeOf(