fix(security): composer drafts no longer survive logout or cross accounts
draft-msg-<roomId> was unscoped and deliberately skipped on logout, then hydrated into whoever logged in next. Wipe drafts in clearPlaintextCaches, and only hydrate a draft whose stored userId matches the current user. Drafts written before this change carry no userId and are dropped on first load (a one-time loss of unsent drafts, accepted for the leak fix). Fixes #41 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
import { Descendant } from 'slate';
|
||||
import { roomIdToMsgDraftAtomFamily } from '../state/room/roomInputDrafts';
|
||||
import { DRAFT_MSG_KEY_PREFIX, hasMsgDraft } from '../utils/draft';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
/**
|
||||
* On startup, pre-fill the per-room message-draft atoms from their localStorage
|
||||
@@ -13,11 +13,19 @@ import { DRAFT_MSG_KEY_PREFIX, hasMsgDraft } from '../utils/draft';
|
||||
* (identical content), so composer restore is unaffected.
|
||||
*
|
||||
* Thread drafts (key contains `::`) are skipped — the nav indicator is room-level.
|
||||
*
|
||||
* [Gitea #41] Drafts are stored as `{ userId, nodes }` (RoomInput's persist
|
||||
* path) so a draft written by a different account never gets hydrated into the
|
||||
* currently logged-in user's session. A legacy draft (stored as a bare array,
|
||||
* pre-dating user-scoping) has no userId to check, so it's treated as foreign
|
||||
* and dropped rather than trusted.
|
||||
*/
|
||||
export function useHydrateMsgDrafts(): void {
|
||||
const store = useStore();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
const userId = mx.getUserId();
|
||||
let keys: string[];
|
||||
try {
|
||||
keys = Object.keys(localStorage);
|
||||
@@ -34,7 +42,19 @@ export function useHydrateMsgDrafts(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (!stored) return;
|
||||
const nodes = JSON.parse(stored) as Descendant[];
|
||||
const parsed = JSON.parse(stored);
|
||||
const foreign =
|
||||
!parsed ||
|
||||
typeof parsed !== 'object' ||
|
||||
Array.isArray(parsed) ||
|
||||
parsed.userId !== userId;
|
||||
if (foreign) {
|
||||
// Another account's (or a pre-scoping legacy) draft — never hydrate it,
|
||||
// and drop it so it can't resurface for the next login either.
|
||||
localStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
const nodes = parsed.nodes;
|
||||
if (Array.isArray(nodes) && hasMsgDraft(nodes)) {
|
||||
store.set(roomIdToMsgDraftAtomFamily(draftKey), nodes);
|
||||
}
|
||||
@@ -42,5 +62,5 @@ export function useHydrateMsgDrafts(): void {
|
||||
// Ignore a malformed stored draft.
|
||||
}
|
||||
});
|
||||
}, [store]);
|
||||
}, [store, mx]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user