BLOCKER: Mail reader cache keeps revoked sender image permission #736

Open
opened 2026-10-02 13:06:55 +00:00 by kayg · 3 comments
Owner

Round 7b defensive review for #427. Branch: job/maillayouts, head f9f360e68f4e.

Severity: P1 privacy leak. Merge blocker. The new Mail reader cache can use a sender's old remote-image permission after the User withdraws it. The browser can then send the User's address and message-open timing to the remote image host.

Evidence:

  • crates/plugins/mail/src/cache/store.rs:1643 reads image permission by (owner_id, sender_address). The removal at line 1663 applies to all messages from that sender.
  • apps/web/src/lib/mail/MailView.svelte:917 changes that sender permission, but lines 921–928 remove and refresh only the selected message ID.
  • apps/web/src/lib/mail/MailView.svelte:548 restores a warmed detail and returns at line 553 without checking the current permission.
  • apps/web/src/lib/mail/MailReaderContent.svelte:56 passes the cached permission to the frame. apps/web/src/lib/mail/frame.ts:131 permits direct HTTPS image requests when that permission is true.

Expected: a sender permission change takes effect for every retained message from that sender before it can render. Invalidate matching pending reads too, so an older completion cannot restore the grant. Prefer a separate current permission model over keeping security decisions inside long-lived body entries. Keep the image-proxy work in #726 consistent with this rule.

Verification: a small local check used the branch's actual BoundedReaderCache. Two inert entries started with the same allowed sender state. After the selected entry was removed and replaced with the refused state, the other entry still returned remote_content_allowed: true. No network requests or hostile payloads were used. Output:

CONFIRMED: another warmed message retains revoked sender image permission.

Regression test idea: warm two messages from one synthetic sender, withdraw the sender's image permission through one message, then select the other. Assert that no remote-image request starts and that its frame uses the current permission. Include a pending prefetch completion and all three reading layouts.

Duplicate search: searched all issue states for remote images and reader cache; read #640, #641 and #726. #726 covers faithful HTML and an image proxy. It does not cover sender permission invalidation in the new reader cache.

Round 7b defensive review for #427. Branch: `job/maillayouts`, head `f9f360e68f4e`. Severity: P1 privacy leak. **Merge blocker.** The new Mail reader cache can use a sender's old remote-image permission after the User withdraws it. The browser can then send the User's address and message-open timing to the remote image host. Evidence: - `crates/plugins/mail/src/cache/store.rs:1643` reads image permission by `(owner_id, sender_address)`. The removal at line 1663 applies to all messages from that sender. - `apps/web/src/lib/mail/MailView.svelte:917` changes that sender permission, but lines 921–928 remove and refresh only the selected message ID. - `apps/web/src/lib/mail/MailView.svelte:548` restores a warmed detail and returns at line 553 without checking the current permission. - `apps/web/src/lib/mail/MailReaderContent.svelte:56` passes the cached permission to the frame. `apps/web/src/lib/mail/frame.ts:131` permits direct HTTPS image requests when that permission is true. Expected: a sender permission change takes effect for every retained message from that sender before it can render. Invalidate matching pending reads too, so an older completion cannot restore the grant. Prefer a separate current permission model over keeping security decisions inside long-lived body entries. Keep the image-proxy work in #726 consistent with this rule. Verification: a small local check used the branch's actual `BoundedReaderCache`. Two inert entries started with the same allowed sender state. After the selected entry was removed and replaced with the refused state, the other entry still returned `remote_content_allowed: true`. No network requests or hostile payloads were used. Output: ```text CONFIRMED: another warmed message retains revoked sender image permission. ``` Regression test idea: warm two messages from one synthetic sender, withdraw the sender's image permission through one message, then select the other. Assert that no remote-image request starts and that its frame uses the current permission. Include a pending prefetch completion and all three reading layouts. Duplicate search: searched all issue states for remote images and reader cache; read #640, #641 and #726. #726 covers faithful HTML and an image proxy. It does not cover sender permission invalidation in the new reader cache.
Author
Owner

Design-sync #864 corrected DESIGN §45 on job/design-sync, commit 01c9bf1f4.

Old docs/DESIGN.md:2173-2174 required consent before remote content loads. The owner comment on #726 dated 2026-10-02 supersedes that rule. DESIGN now records automatic server image-proxy loading, fetch at delivery or first sync, no cookies or Referer, size/type limits, silent tracking-pixel removal and the one default-on Settings switch. No reader Load images button or blocked notice. Sender layout and colours stay; dark adaptation is in ⋯.

#736's cache requirement stays: check current preference and access before showing retained content. Regression idea: no browser request to a remote image host; no upstream fetch on message open; tracking pixels stay absent; changing the setting affects retained content. No runtime claim or test run from this LIGHT documentation job.

Design-sync #864 corrected DESIGN §45 on `job/design-sync`, commit `01c9bf1f4`. Old docs/DESIGN.md:2173-2174 required consent before remote content loads. The owner comment on #726 dated 2026-10-02 supersedes that rule. DESIGN now records automatic server image-proxy loading, fetch at delivery or first sync, no cookies or Referer, size/type limits, silent tracking-pixel removal and the one default-on Settings switch. No reader Load images button or blocked notice. Sender layout and colours stay; dark adaptation is in ⋯. #736's cache requirement stays: check current preference and access before showing retained content. Regression idea: no browser request to a remote image host; no upstream fetch on message open; tracking pixels stay absent; changing the setting affects retained content. No runtime claim or test run from this LIGHT documentation job.
Author
Owner

Independent read-only review of job/mailhtml-726 at
76a79589e338d17dfe3f4c503b8a6e7c8b43e075, for #726 and #736.
No build, test, server or browser was used.

P1: cached-image access differs between read paths.
crates/plugins/mail/src/routes.rs:1423 checks the current User preference,
then line 1432 calls remote::hydrate. The query at
crates/plugins/mail/src/remote.rs:413 checks owner, account and image ID but
not whether the Connected Account is enabled. The message query at
crates/plugins/mail/src/cache/store.rs:1451 also lacks that check. The
direct image route at routes.rs:1480 does require a.enabled=1.
A new detail request can thus return cached remote bytes in data URLs after
the account is disabled, while the image route refuses the same image.

Fix: use one current-access check before both image reads. Keep local message
text if that is the intended account policy. Regression idea: prepare an
owned remote image, disable its account, and read both routes. Neither may
return remote image bytes. Include another User and a missing image.

P2: another tab retains the old content preference.
apps/web/src/routes/settings/mail/MailSection.svelte:120 clears only that
JavaScript context's cache after the setting is saved. Warm selection at
apps/web/src/lib/mail/MailView.svelte:576 returns retained bodies without a
current-preference check. apps/web/src/lib/mail/frame.ts:47 ignores the
allowance argument because remote bytes are embedded. There is no preference
revision or cross-tab notification in these paths. An API preference change
also leaves the warm body intact.

Fix: keep current preference state outside cached bodies, notify readers of
changes, and fence pending work by revision. Revalidate before warm rendering.
Regression idea: warm two messages in two tabs, turn off Load remote content,
and select both messages. Include a delayed completion. The data-only CSP
prevents a direct sender request; this finding does not claim a User-IP or
message-open leak.

DESIGN §45 and #736 require current preference and access before cached
content is shown. Duplicate searches covered all states for remote, revoked
and hydrate terms. These findings extend #736; no duplicate issue was opened.
Full review: review-mailhtml-726.md and audit-findings.md on
job/rev2-mailhtml-726. No product code was changed.

Independent read-only review of `job/mailhtml-726` at `76a79589e338d17dfe3f4c503b8a6e7c8b43e075`, for #726 and #736. No build, test, server or browser was used. **P1: cached-image access differs between read paths.** `crates/plugins/mail/src/routes.rs:1423` checks the current User preference, then line 1432 calls `remote::hydrate`. The query at `crates/plugins/mail/src/remote.rs:413` checks owner, account and image ID but not whether the Connected Account is enabled. The message query at `crates/plugins/mail/src/cache/store.rs:1451` also lacks that check. The direct image route at `routes.rs:1480` does require `a.enabled=1`. A new detail request can thus return cached remote bytes in data URLs after the account is disabled, while the image route refuses the same image. Fix: use one current-access check before both image reads. Keep local message text if that is the intended account policy. Regression idea: prepare an owned remote image, disable its account, and read both routes. Neither may return remote image bytes. Include another User and a missing image. **P2: another tab retains the old content preference.** `apps/web/src/routes/settings/mail/MailSection.svelte:120` clears only that JavaScript context's cache after the setting is saved. Warm selection at `apps/web/src/lib/mail/MailView.svelte:576` returns retained bodies without a current-preference check. `apps/web/src/lib/mail/frame.ts:47` ignores the allowance argument because remote bytes are embedded. There is no preference revision or cross-tab notification in these paths. An API preference change also leaves the warm body intact. Fix: keep current preference state outside cached bodies, notify readers of changes, and fence pending work by revision. Revalidate before warm rendering. Regression idea: warm two messages in two tabs, turn off Load remote content, and select both messages. Include a delayed completion. The data-only CSP prevents a direct sender request; this finding does not claim a User-IP or message-open leak. DESIGN §45 and #736 require current preference and access before cached content is shown. Duplicate searches covered all states for remote, revoked and hydrate terms. These findings extend #736; no duplicate issue was opened. Full review: `review-mailhtml-726.md` and `audit-findings.md` on `job/rev2-mailhtml-726`. No product code was changed.
Author
Owner

Fixes for F1 and F2 (this blocker) are on job/mailhtml-726 at b45b94fb0, with regression tests. Details and gate output are in the #726 comment.

Fixes for F1 and F2 (this blocker) are on `job/mailhtml-726` at `b45b94fb0`, with regression tests. Details and gate output are in the #726 comment.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#736
No description provided.