fix(security): widget consents and shared files cleared on logout; forged shares refused (#266, #267)
CI / Build & Quality Checks (pull_request) Successful in 1m53s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 7s
CI / Playwright smoke (e2e) (pull_request) Successful in 12m51s
CI / Build & Quality Checks (pull_request) Successful in 1m53s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 7s
CI / Playwright smoke (e2e) (pull_request) Successful in 12m51s
#266: remembered widget approvals (lotus.widgetConsent.*) survived logout, so they applied silently to the next account on the device and listed the previous account's rooms; clearPlaintextCaches now sweeps them. Space structure, call membership and Lotus room-policy state join PROTECTED_STATE_TYPES, so no widget can be granted them. #267: any website could auto-submit a form to /share-target and land the user in the share picker with attacker-chosen text and files. The worker now refuses a share whose Origin is another site. (Sec-Fetch-Site, as the issue suggested, never reaches a service worker: checked in Chromium.) Origin null/absent, as the OS share sheet sends, stays accepted, and /share now says the content came from another app or website. Both logout paths await clearSharedPayload(); the server-forced one also clears the soundboard clip cache, as #57 intended. Fixes #266 Fixes #267 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a5471a0576
commit
14f05a3d5c
@@ -106,3 +106,19 @@ test('classifyWidgetCapabilities: reading protected state is fine, changing unpr
|
||||
test('loadWidgetConsent tolerates missing storage', () => {
|
||||
assert.equal(loadWidgetConsent('!r:x', 'w', 'https://w.example'), undefined);
|
||||
});
|
||||
|
||||
test('[#266] space structure, call membership and Lotus room policy can never be granted', () => {
|
||||
const { auto, ask } = classifyWidgetCapabilities(
|
||||
[
|
||||
'm.space.child',
|
||||
'm.space.parent',
|
||||
'org.matrix.msc3401.call',
|
||||
'org.matrix.msc3401.call.member',
|
||||
'm.call.member',
|
||||
'io.lotus.voice_limit',
|
||||
'io.lotus.room_quality',
|
||||
].map((t) => `org.matrix.msc2762.send.state_event:${t}`),
|
||||
);
|
||||
assert.equal(auto.size, 0);
|
||||
assert.deepEqual(ask, []);
|
||||
});
|
||||
|
||||
@@ -67,6 +67,16 @@ export const PROTECTED_STATE_TYPES: ReadonlySet<string> = new Set([
|
||||
'm.room.third_party_invite',
|
||||
'im.vector.modular.widgets',
|
||||
'm.widget',
|
||||
// [Gitea #266] Space structure, call membership and Lotus room policy: a
|
||||
// widget could restructure a space, fake call membership or lift the
|
||||
// voice-channel limit, so these can't be granted even with consent.
|
||||
'm.space.child',
|
||||
'm.space.parent',
|
||||
'org.matrix.msc3401.call',
|
||||
'org.matrix.msc3401.call.member',
|
||||
'm.call.member',
|
||||
'io.lotus.voice_limit',
|
||||
'io.lotus.room_quality',
|
||||
]);
|
||||
|
||||
export type WidgetPermissionRequest = {
|
||||
@@ -137,8 +147,11 @@ export const classifyWidgetCapabilities = (
|
||||
*/
|
||||
export type StoredWidgetConsent = { url: string; allowed: Capability[]; denied: Capability[] };
|
||||
|
||||
// [Gitea #266] Swept on logout by clearPlaintextCaches, so one account's
|
||||
// approvals never apply to the next account on the same device.
|
||||
export const WIDGET_CONSENT_PREFIX = 'lotus.widgetConsent.';
|
||||
export const widgetConsentKey = (roomId: string, widgetId: string): string =>
|
||||
`lotus.widgetConsent.${roomId}.${widgetId}`;
|
||||
`${WIDGET_CONSENT_PREFIX}${roomId}.${widgetId}`;
|
||||
|
||||
export const loadWidgetConsent = (
|
||||
roomId: string,
|
||||
|
||||
Reference in New Issue
Block a user