mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
Scope email calendar extraction to account owner
The email auto-calendar pass (settings.email_auto_calendar / the extract_email_events task) scans recently received mail and lets an LLM create / update / cancel calendar events. Two problems made it a cross-tenant, remotely triggerable hole: 1. No owner scoping. _auto_summarize_pass(account_id=None) fans out over EVERY enabled account of EVERY user. For each message it fetched an upcoming-events snapshot with NO owner filter (all tenants' events) and handed those uids + titles to the extraction LLM, then executed the model's ops via do_manage_calendar(...) with owner=None. do_manage_calendar only filters by owner when owner is not None, so create/update/delete ran across ALL users' calendars. Net: every user's event titles/times were disclosed to the model, and the model could cancel/move/duplicate any tenant's events by uid. 2. No prompt-injection wrapping. The raw email From/Subject/body were interpolated straight into an instruction-shaped extraction prompt (unlike the chat path, which wraps external text via src/prompt_security). Anyone who can email a user whose instance has auto-calendar enabled could inject operations: create attacker-controlled "meeting" events (the path even auto-harvests URLs from the body into the event location/description — a phishing primitive) or cancel/modify the victim's real events, with zero human in the loop. Fix: - Add core.database.get_upcoming_events(owner) and use it for the snapshot, so the LLM only ever sees the processed account owner's events. - Look up the EmailAccount owner in _auto_summarize_pass_single and pass owner= to every do_manage_calendar call, so create/update/delete are scoped to that user (owner=None stays the single-user / legacy escape hatch). - Tell the extraction model the email is untrusted data and not to follow instructions inside it (defense-in-depth against injection). Add tests/test_calendar_owner_scope.py: get_upcoming_events returns only the given owner's events (and everything when owner is None). Fails against the old unscoped query.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
"""Pin owner-scoping of the autonomous email->calendar event snapshot.
|
||||
|
||||
The email auto-calendar pass fans out over EVERY user's mailbox and used to
|
||||
feed an *unscoped* upcoming-events snapshot to the extraction LLM, then execute
|
||||
the model's create/update/delete ops via do_manage_calendar with owner=None —
|
||||
so processing one tenant's mail could read AND mutate another tenant's calendar
|
||||
(and leak every tenant's event titles to the LLM endpoint).
|
||||
|
||||
The fix routes the snapshot through core.database.get_upcoming_events(owner)
|
||||
and passes the account owner to do_manage_calendar. This test pins that
|
||||
get_upcoming_events scopes to the owner; it fails if the owner filter is
|
||||
dropped (the original cross-tenant behavior).
|
||||
"""
|
||||
import os
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from core import database as db
|
||||
|
||||
|
||||
def test_get_upcoming_events_is_owner_scoped():
|
||||
db.Base.metadata.create_all(bind=db.engine)
|
||||
soon = datetime.utcnow() + timedelta(days=2)
|
||||
end = soon + timedelta(hours=1)
|
||||
|
||||
s = db.SessionLocal()
|
||||
try:
|
||||
s.merge(db.CalendarCal(id="cal-alice", owner="alice", name="Alice"))
|
||||
s.merge(db.CalendarCal(id="cal-bob", owner="bob", name="Bob"))
|
||||
s.merge(db.CalendarEvent(uid="ev-alice", calendar_id="cal-alice",
|
||||
summary="Alice 1:1", dtstart=soon, dtend=end))
|
||||
s.merge(db.CalendarEvent(uid="ev-bob", calendar_id="cal-bob",
|
||||
summary="Bob 1:1", dtstart=soon, dtend=end))
|
||||
s.commit()
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
alice = {e["uid"] for e in db.get_upcoming_events("alice")}
|
||||
bob = {e["uid"] for e in db.get_upcoming_events("bob")}
|
||||
everyone = {e["uid"] for e in db.get_upcoming_events(None)}
|
||||
|
||||
# An owner sees ONLY their own events — never the other tenant's.
|
||||
assert alice == {"ev-alice"}, alice
|
||||
assert bob == {"ev-bob"}, bob
|
||||
assert "ev-bob" not in alice and "ev-alice" not in bob
|
||||
# owner=None is the explicit single-user / legacy escape hatch (unscoped).
|
||||
assert {"ev-alice", "ev-bob"} <= everyone
|
||||
Reference in New Issue
Block a user