|
1 | | -import { useEffect, useRef } from 'react'; |
2 | | -import { useLocation, useNavigate } from 'react-router-dom'; |
| 1 | +import { useEffect, useRef, useCallback } from 'react'; |
| 2 | +import { useLocation } from 'react-router-dom'; |
| 3 | +import { getSafeRouterPath } from '../utilities/functions'; |
3 | 4 |
|
| 5 | +/** |
| 6 | + * Custom hook to block browser navigation when a modal is open. |
| 7 | + * Uses stored pathname from React Router to avoid Open Redirect vulnerabilities (CWE-601). |
| 8 | + */ |
4 | 9 | const useBlockNavigation = (isModalOpen: boolean) => { |
5 | 10 | const location = useLocation(); |
6 | | - const navigate = useNavigate(); |
7 | | - const initialPathnameRef = useRef(location.pathname); |
| 11 | + |
| 12 | + // Store the validated pathname when modal state changes |
| 13 | + // This breaks the data flow from user-controlled input to redirect |
| 14 | + const storedPathnameRef = useRef<string>('/'); |
| 15 | + |
| 16 | + // Memoized function to get the safe stored path |
| 17 | + const getSafeStoredPath = useCallback(() => { |
| 18 | + return storedPathnameRef.current; |
| 19 | + }, []); |
| 20 | + |
| 21 | + // Update stored pathname only when modal is not open |
| 22 | + // This captures the safe path before any manipulation |
| 23 | + useEffect(() => { |
| 24 | + if (!isModalOpen) { |
| 25 | + // Store the current path from React Router's validated state |
| 26 | + storedPathnameRef.current = getSafeRouterPath(location); |
| 27 | + } |
| 28 | + }, [isModalOpen, location]); |
8 | 29 |
|
9 | 30 | useEffect(() => { |
10 | | - const handlePopState = (event: PopStateEvent) => { |
11 | | - // If the modal is open, prevent navigation |
| 31 | + const handlePopState = () => { |
| 32 | + // If the modal is open, prevent navigation by pushing state with stored safe path |
12 | 33 | if (isModalOpen) { |
13 | | - window.history.pushState(null, '', window.location.pathname); |
14 | | - navigate(location.pathname); |
| 34 | + const safePath = getSafeStoredPath(); |
| 35 | + window.history.pushState({ blockNav: true }, '', safePath); |
15 | 36 | } |
16 | 37 | }; |
17 | 38 |
|
18 | 39 | if (isModalOpen) { |
19 | | - initialPathnameRef.current = location.pathname; |
20 | | - window.history.pushState(null, '', window.location.pathname); |
| 40 | + // Store the current safe path when modal opens |
| 41 | + storedPathnameRef.current = getSafeRouterPath(location); |
| 42 | + const safePath = getSafeStoredPath(); |
| 43 | + window.history.pushState({ blockNav: true }, '', safePath); |
21 | 44 | window.addEventListener('popstate', handlePopState); |
22 | 45 | } |
23 | 46 |
|
24 | 47 | return () => { |
25 | 48 | window.removeEventListener('popstate', handlePopState); |
26 | 49 | }; |
27 | | - }, [isModalOpen, navigate, location.pathname]); |
28 | | - |
29 | | - useEffect(() => { |
30 | | - if (!isModalOpen) { |
31 | | - initialPathnameRef.current = location.pathname; |
32 | | - } |
33 | | - }, [isModalOpen, location.pathname]); |
| 50 | + }, [isModalOpen, getSafeStoredPath, location]); |
34 | 51 | }; |
35 | 52 |
|
36 | 53 | export default useBlockNavigation; |
0 commit comments