Share target: any website can pre-fill the share page with its own text/files, and an unplaced share survives logout #309

Closed
opened 2026-10-05 00:06:16 -04:00 by jared · 1 comment
Owner

The PWA share target (#155, 96a97a2f) works as designed from the Android share sheet. Verified locally through the service worker and on prod:

  • POST https://chat.lotusguild.org/share-target answers 303 → /share;
  • /share lists the file and text;
  • picking a room puts the file on that room's upload board and "Bug report\nLook at this https://example.org/x" in its draft;
  • the stash is then cleared.

Two gaps:

1. Anyone can send to it, not just the share sheet. The worker's fetch handler (src/sw.ts:240) and stashSharedForm (src/swShare.ts) accept any POST to /share-target. A web page can auto-submit a multipart form to https://chat.lotusguild.org/share-target; the Lotus service worker handles cross-site navigations too. In the test, a data: page posted text=Free nitro: https://evil.example/claim plus a JPEG, and the browser landed on Lotus's "Share to Lotus Chat" page showing exactly that, ready for "Send to ".

The user still has to pick a room and press Send, so this is a social-engineering aid rather than a direct exploit. Example lure: "share your crash log with Lotus support" → a malicious link and file in a group chat, appearing to come from the user. It also overwrites anything legitimately shared but not yet placed, because stashSharedForm clears the cache first.

Options:

  • Reject the POST when event.request.referrer is a foreign origin. Share-sheet POSTs carry no referrer; this doesn't stop no-referrer pages, but it raises the bar.
  • On /share, say plainly where the content came from ("Shared from another app or website: check it before sending") and don't preselect anything.
  • Expire stashes after a few minutes (receivedAt is already stored).

2. Logout leaves the stash behind. Share something, don't pick a room, then log out. The lotus-share-target Cache API bucket still holds the payload and the file:

{"text":"private note never placed","files":[{"name":"shared-photo.jpg","type":"image/jpeg","size":2401}],…}

The next account to log in on that browser and open /share gets it. clearPlaintextCaches (src/app/state/plaintextCaches.ts) wipes drafts, the outbox and the search index, but doesn't call clearSharedPayload(). Add it there, and add a case to the logout-wipe test.


Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny 30e2dbcd, cinny-desktop 8f67ec7, element-call 3b0050e8.

The PWA share target (#155, 96a97a2f) works as designed from the Android share sheet. Verified locally through the service worker and on prod: - `POST https://chat.lotusguild.org/share-target` answers 303 → `/share`; - `/share` lists the file and text; - picking a room puts the file on that room's upload board and "Bug report\nLook at this https://example.org/x" in its draft; - the stash is then cleared. Two gaps: **1. Anyone can send to it, not just the share sheet.** The worker's `fetch` handler (`src/sw.ts:240`) and `stashSharedForm` (`src/swShare.ts`) accept any POST to `/share-target`. A web page can auto-submit a multipart form to `https://chat.lotusguild.org/share-target`; the Lotus service worker handles cross-site navigations too. In the test, a `data:` page posted `text=Free nitro: https://evil.example/claim` plus a JPEG, and the browser landed on Lotus's **"Share to Lotus Chat"** page showing exactly that, ready for "Send to <room>". The user still has to pick a room and press Send, so this is a social-engineering aid rather than a direct exploit. Example lure: "share your crash log with Lotus support" → a malicious link and file in a group chat, appearing to come from the user. It also overwrites anything legitimately shared but not yet placed, because `stashSharedForm` clears the cache first. Options: - Reject the POST when `event.request.referrer` is a foreign origin. Share-sheet POSTs carry no referrer; this doesn't stop `no-referrer` pages, but it raises the bar. - On `/share`, say plainly where the content came from ("Shared from another app or website: check it before sending") and don't preselect anything. - Expire stashes after a few minutes (`receivedAt` is already stored). **2. Logout leaves the stash behind.** Share something, don't pick a room, then log out. The `lotus-share-target` Cache API bucket still holds the payload and the file: ``` {"text":"private note never placed","files":[{"name":"shared-photo.jpg","type":"image/jpeg","size":2401}],…} ``` The next account to log in on that browser and open `/share` gets it. `clearPlaintextCaches` (src/app/state/plaintextCaches.ts) wipes drafts, the outbox and the search index, but doesn't call `clearSharedPayload()`. Add it there, and add a case to the logout-wipe test. --- _Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny `30e2dbcd`, cinny-desktop `8f67ec7`, element-call `3b0050e8`._
jared added the priority: lowsecurityarea: mobile labels 2026-10-05 00:06:16 -04:00
Author
Owner

Duplicate of #267 (filed in round 1). The new evidence from this pass has been added there as a comment.

Duplicate of #267 (filed in round 1). The new evidence from this pass has been added there as a comment.
jared added the duplicate label 2026-10-05 01:15:47 -04:00
jared closed this issue 2026-10-05 01:15:47 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#309