From 5dca93a1af0fbc27b532d59bc1f12e39f940a265 Mon Sep 17 00:00:00 2001 From: Vox-d-glitch Date: Sat, 27 Jun 2026 22:39:36 +0100 Subject: [PATCH] fix(clipboard): add 30-second TTL to clear pasted content from state Clipboard content held in JS heap memory is a security risk on iOS 16+ and Android 12+, where reads also trigger a visible system toast. A 30-second useEffect timeout now nullifies clipboardContent automatically, with proper cleanup on unmount. No clipboard data is forwarded to loggers or analytics. Adds a fake-timer unit test asserting the auto-clear. --- .../hooks/useOptimizedClipboard.test.ts | 24 ++++++++++++- src/hooks/useOptimizedClipboard.ts | 34 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/__tests__/hooks/useOptimizedClipboard.test.ts b/src/__tests__/hooks/useOptimizedClipboard.test.ts index 715f8cef..2cac3c60 100644 --- a/src/__tests__/hooks/useOptimizedClipboard.test.ts +++ b/src/__tests__/hooks/useOptimizedClipboard.test.ts @@ -30,7 +30,7 @@ describe('useOptimizedClipboard', () => { expect(result.current.isPasting).toBe(false); expect(result.current.copySuccess).toBe(false); expect(result.current.error).toBeNull(); - expect(result.current.clipboardContent).toBe(''); + expect(result.current.clipboardContent).toBeNull(); expect(result.current.metrics).toBeNull(); }); @@ -110,4 +110,26 @@ describe('useOptimizedClipboard', () => { expect(result.current.clipboardContent).toBe('test pasted text'); expect(result.current.error).toBeNull(); }); + + it('clears clipboard content from state 30 seconds after paste', async () => { + const { result } = renderHook(() => useOptimizedClipboard()); + + let promise: Promise; + act(() => { + promise = result.current.pasteFromClipboard(); + }); + + await act(async () => { + jest.runAllTimers(); + await promise; + }); + + expect(result.current.clipboardContent).toBe('test pasted text'); + + act(() => { + jest.advanceTimersByTime(30_000); + }); + + expect(result.current.clipboardContent).toBeNull(); + }); }); diff --git a/src/hooks/useOptimizedClipboard.ts b/src/hooks/useOptimizedClipboard.ts index d61fad1b..1f5101ef 100644 --- a/src/hooks/useOptimizedClipboard.ts +++ b/src/hooks/useOptimizedClipboard.ts @@ -8,23 +8,33 @@ export interface UseOptimizedClipboardResult { isPasting: boolean; copySuccess: boolean; error: Error | null; - clipboardContent: string; + clipboardContent: string | null; metrics: ClipboardOperationMetrics | null; copyToClipboard: (text: string) => Promise; pasteFromClipboard: () => Promise; clearError: () => void; } +/** + * Optimized clipboard hook with a 30-second TTL on pasted content. + * + * Clipboard access is only performed in response to explicit user gestures — + * never on mount or auto-focus. Pasted content is automatically cleared from + * state after 30 s (TTL) to limit the exposure window for sensitive data such + * as passwords, auth tokens, and payment card numbers held in JS heap memory. + * No clipboard content is forwarded to analytics or error-reporting services. + */ export function useOptimizedClipboard(): UseOptimizedClipboardResult { const [isCopying, setIsCopying] = useState(false); const [isPasting, setIsPasting] = useState(false); const [copySuccess, setCopySuccess] = useState(false); const [error, setError] = useState(null); - const [clipboardContent, setClipboardContent] = useState(''); + const [clipboardContent, setClipboardContent] = useState(null); const [metrics, setMetrics] = useState(null); - + const isMounted = useRef(true); const successTimeoutRef = useRef(null); + const clipboardTtlRef = useRef(null); useEffect(() => { isMounted.current = true; @@ -36,6 +46,24 @@ export function useOptimizedClipboard(): UseOptimizedClipboardResult { }; }, []); + // Clear clipboard content from state 30 s after it is set to limit the + // in-memory exposure window for sensitive data. + useEffect(() => { + if (clipboardContent !== null) { + clipboardTtlRef.current = setTimeout(() => { + if (isMounted.current) { + setClipboardContent(null); + } + }, 30_000); + } + return () => { + if (clipboardTtlRef.current) { + clearTimeout(clipboardTtlRef.current); + clipboardTtlRef.current = null; + } + }; + }, [clipboardContent]); + const clearError = useCallback(() => { setError(null); }, []);