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