Share target: any website can POST into /share-target, and stashed shared files aren't cleared on logout #267

Closed
opened 2026-10-04 17:41:28 -04:00 by jared · 1 comment
Owner

The service worker accepts any navigation POST to /share-target (src/swShare.ts:24-55). It stashes the form's text and files in Cache Storage (lotus-share-target) and redirects to /share.

1. Forged shares
Any website can auto-submit a cross-site form there. The user then lands in Lotus Chat's "Share to…" picker with attacker-chosen text and files pre-attached, which looks like it came from their own share sheet. It still needs the user to pick a room and send, so the risk is low (social engineering).

  • Fix: require Sec-Fetch-Site: none / same-origin (the Android share sheet sends none), or show where the share came from.

2. Leftover files after logout
Stashed files stay in Cache Storage until someone opens /share. clearPlaintextCaches doesn't call clearSharedPayload(), so a shared photo can outlive a logout on that device.

  • Fix: call clearSharedPayload() from clearPlaintextCaches.

More context

Introduced by 96a97a2f (2026-09-20, the share target #155).

Confirmed (dev SW, Playwright): a page on another origin auto-submitted

<form method=POST enctype=multipart/form-data action="https://chat.lotusguild.org/share-target">
  <input name=title value="Urgent">
  <input name=text value="Your account will be suspended — verify at https://evil.example/verify">
</form>

The browser landed on /share showing "Share to Lotus Chat · Shared · Urgent · Your account will be suspended — verify at https://evil.example/verify · Send to …" with the room list ready.

Fix

  • In sw.ts, only accept the POST when request.headers.get('Sec-Fetch-Site') is none (that's what the OS share sheet sends) or same-origin; otherwise redirect to / and drop the body.
  • Add clearSharedPayload() to clearPlaintextCaches.

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 service worker accepts any navigation `POST` to `/share-target` (src/swShare.ts:24-55). It stashes the form's text and files in Cache Storage (`lotus-share-target`) and redirects to `/share`. **1. Forged shares** Any website can auto-submit a cross-site form there. The user then lands in Lotus Chat's "Share to…" picker with **attacker-chosen text and files** pre-attached, which looks like it came from their own share sheet. It still needs the user to pick a room and send, so the risk is low (social engineering). - **Fix:** require `Sec-Fetch-Site: none` / `same-origin` (the Android share sheet sends `none`), or show where the share came from. **2. Leftover files after logout** Stashed files stay in Cache Storage until someone opens `/share`. `clearPlaintextCaches` doesn't call `clearSharedPayload()`, so a shared photo can outlive a logout on that device. - **Fix:** call `clearSharedPayload()` from `clearPlaintextCaches`. ### More context **Introduced by** 96a97a2f (2026-09-20, the share target #155). **Confirmed (dev SW, Playwright):** a page on another origin auto-submitted ```html <form method=POST enctype=multipart/form-data action="https://chat.lotusguild.org/share-target"> <input name=title value="Urgent"> <input name=text value="Your account will be suspended — verify at https://evil.example/verify"> </form> ``` The browser landed on `/share` showing **"Share to Lotus Chat · Shared · Urgent · Your account will be suspended — verify at https://evil.example/verify · Send to …"** with the room list ready. **Fix** - In `sw.ts`, only accept the POST when `request.headers.get('Sec-Fetch-Site')` is `none` (that's what the OS share sheet sends) or `same-origin`; otherwise redirect to `/` and drop the body. - Add `clearSharedPayload()` to `clearPlaintextCaches`. --- _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: messaging labels 2026-10-04 17:41:28 -04:00
Author
Owner

Re-verified 2026-10-05 (Playwright, through the service worker), with more detail:

  • Forged share: a data: page auto-submitted text=Free nitro: https://evil.example/claim plus a JPEG. The browser landed on "Share to Lotus Chat" showing exactly that, ready for "Send to ". Also: stashSharedForm clears the cache first, so a forged POST replaces anything the user legitimately shared but hadn't placed yet.
  • Logout: after logout, lotus-share-target still held {"text":"private note never placed","files":[{"name":"shared-photo.jpg",…}]}. clearPlaintextCaches doesn't call clearSharedPayload().
  • Fix caveat: I'm not sure the Sec-Fetch-* headers are visible to the service worker's fetch handler. If they aren't, a request.referrer check (empty for the share sheet) plus a short expiry on receivedAt is the practical guard.
  • Working: POST https://chat.lotusguild.org/share-target answers 303 → /share on prod. Locally, the SW stashes the share; picking a room puts the file on that room's upload board and the text in its draft, then clears the stash.

(This was accidentally re-filed as #309, now closed as a duplicate.)

**Re-verified 2026-10-05 (Playwright, through the service worker), with more detail:** - **Forged share:** a `data:` page auto-submitted `text=Free nitro: https://evil.example/claim` plus a JPEG. The browser landed on **"Share to Lotus Chat"** showing exactly that, ready for "Send to <room>". Also: `stashSharedForm` clears the cache first, so a forged POST replaces anything the user legitimately shared but hadn't placed yet. - **Logout:** after logout, `lotus-share-target` still held `{"text":"private note never placed","files":[{"name":"shared-photo.jpg",…}]}`. `clearPlaintextCaches` doesn't call `clearSharedPayload()`. - **Fix caveat:** I'm not sure the `Sec-Fetch-*` headers are visible to the service worker's `fetch` handler. If they aren't, a `request.referrer` check (empty for the share sheet) plus a short expiry on `receivedAt` is the practical guard. - **Working:** `POST https://chat.lotusguild.org/share-target` answers 303 → `/share` on prod. Locally, the SW stashes the share; picking a room puts the file on that room's upload board and the text in its draft, then clears the stash. (This was accidentally re-filed as #309, now closed as a duplicate.)
jared closed this issue 2026-10-19 15:18:59 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#267