mirror of https://github.com/grafana/grafana
Alerting: Add useReturnTo hook to safely handle returnTo parameter (#96474)
Add useReturnTo hook to safely handle returnTo parameter Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>pull/95008/head^2
parent
8375fcd350
commit
54cc666aa0
@ -0,0 +1,48 @@ |
||||
import { MemoryRouter } from 'react-router-dom-v5-compat'; |
||||
import { renderHook } from 'test/test-utils'; |
||||
|
||||
import { useReturnTo } from './useReturnTo'; |
||||
|
||||
describe('useReturnTo', () => { |
||||
beforeAll(() => { |
||||
// @ts-expect-error
|
||||
delete window.location; |
||||
window.location = { origin: 'https://play.grafana.net' } as Location; |
||||
}); |
||||
|
||||
it('should return the fallback value when `returnTo` is not present in the query string', () => { |
||||
const { result } = renderHook(() => useReturnTo('/fallback'), { wrapper: MemoryRouter }); |
||||
|
||||
expect(result.current.returnTo).toBe('/fallback'); |
||||
}); |
||||
|
||||
it('should return the sanitized `returnTo` value when it is present in the query string and is a valid URL within the Grafana app', () => { |
||||
const { result } = renderHook(() => useReturnTo('/fallback'), { |
||||
wrapper: ({ children }) => ( |
||||
<MemoryRouter initialEntries={[{ search: '?returnTo=/dashboard/db/my-dashboard' }]}>{children}</MemoryRouter> |
||||
), |
||||
}); |
||||
|
||||
expect(result.current.returnTo).toBe('/dashboard/db/my-dashboard'); |
||||
}); |
||||
|
||||
it('should return the fallback value when `returnTo` is present in the query string but is not a valid URL within the Grafana app', () => { |
||||
const { result } = renderHook(() => useReturnTo('/fallback'), { |
||||
wrapper: ({ children }) => ( |
||||
<MemoryRouter initialEntries={[{ search: '?returnTo=https://example.com' }]}>{children}</MemoryRouter> |
||||
), |
||||
}); |
||||
|
||||
expect(result.current.returnTo).toBe('/fallback'); |
||||
}); |
||||
|
||||
it('should return the fallback value when `returnTo` is present in the query string but is a malicious JavaScript URL', () => { |
||||
const { result } = renderHook(() => useReturnTo('/fallback'), { |
||||
wrapper: ({ children }) => ( |
||||
<MemoryRouter initialEntries={[{ search: '?returnTo=javascript:alert(1)' }]}>{children}</MemoryRouter> |
||||
), |
||||
}); |
||||
|
||||
expect(result.current.returnTo).toBe('/fallback'); |
||||
}); |
||||
}); |
@ -0,0 +1,49 @@ |
||||
import { textUtil } from '@grafana/data'; |
||||
import { config } from '@grafana/runtime'; |
||||
|
||||
import { logWarning } from '../Analytics'; |
||||
|
||||
import { useURLSearchParams } from './useURLSearchParams'; |
||||
|
||||
/** |
||||
* This hook provides a safe way to obtain the `returnTo` URL from the query string parameter |
||||
* It validates the origin and protocol to ensure the URL is withing the Grafana app |
||||
*/ |
||||
export function useReturnTo(fallback?: string): { returnTo: string | undefined } { |
||||
const emptyResult = { returnTo: fallback }; |
||||
|
||||
const [searchParams] = useURLSearchParams(); |
||||
const returnTo = searchParams.get('returnTo'); |
||||
|
||||
if (!returnTo) { |
||||
return emptyResult; |
||||
} |
||||
|
||||
const sanitizedReturnTo = textUtil.sanitizeUrl(returnTo); |
||||
const baseUrl = `${window.location.origin}/${config.appSubUrl}`; |
||||
|
||||
const sanitizedUrl = tryParseURL(sanitizedReturnTo, baseUrl); |
||||
|
||||
if (!sanitizedUrl) { |
||||
logWarning('Malformed returnTo parameter', { returnTo }); |
||||
return emptyResult; |
||||
} |
||||
|
||||
const { protocol, origin, pathname, search } = sanitizedUrl; |
||||
if (['http:', 'https:'].includes(protocol) === false || origin !== window.location.origin) { |
||||
logWarning('Malformed returnTo parameter', { returnTo }); |
||||
return emptyResult; |
||||
} |
||||
|
||||
return { returnTo: `${pathname}${search}` }; |
||||
} |
||||
|
||||
// Tries to mimic URL.parse method https://developer.mozilla.org/en-US/docs/Web/API/URL/parse_static
|
||||
function tryParseURL(sanitizedReturnTo: string, baseUrl: string) { |
||||
try { |
||||
const url = new URL(sanitizedReturnTo, baseUrl); |
||||
return url; |
||||
} catch (error) { |
||||
return null; |
||||
} |
||||
} |
Loading…
Reference in new issue