feat: moves getSafeRedirect into payload package (#12593)

This commit is contained in:
Jarrod Flesch
2025-05-29 09:36:09 -04:00
committed by GitHub
parent d85909e5ae
commit 7e873a9d63
5 changed files with 40 additions and 26 deletions

View File

@@ -16,11 +16,10 @@ import {
useConfig,
useTranslation,
} from '@payloadcms/ui'
import { formatAdminURL, getLoginOptions } from 'payload/shared'
import { formatAdminURL, getLoginOptions, getSafeRedirect } from 'payload/shared'
import type { LoginFieldProps } from '../LoginField/index.js'
import { getSafeRedirect } from '../../../utilities/getSafeRedirect.js'
import { LoginField } from '../LoginField/index.js'
import './index.scss'
@@ -92,7 +91,7 @@ export const LoginForm: React.FC<{
initialState={initialState}
method="POST"
onSuccess={handleLogin}
redirect={getSafeRedirect(searchParams?.redirect, adminRoute)}
redirect={getSafeRedirect({ fallbackTo: adminRoute, redirectTo: searchParams?.redirect })}
waitForAutocomplete
>
<div className={`${baseClass}__inputWrap`}>

View File

@@ -2,10 +2,10 @@ import type { AdminViewServerProps, ServerProps } from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { redirect } from 'next/navigation.js'
import { getSafeRedirect } from 'payload/shared'
import React, { Fragment } from 'react'
import { Logo } from '../../elements/Logo/index.js'
import { getSafeRedirect } from '../../utilities/getSafeRedirect.js'
import { LoginForm } from './LoginForm/index.js'
import './index.scss'
export const loginBaseClass = 'login'
@@ -25,7 +25,7 @@ export function LoginView({ initPageResult, params, searchParams }: AdminViewSer
routes: { admin },
} = config
const redirectUrl = getSafeRedirect(searchParams.redirect, admin)
const redirectUrl = getSafeRedirect({ fallbackTo: admin, redirectTo: searchParams.redirect })
if (user) {
redirect(redirectUrl)

View File

@@ -38,7 +38,6 @@ export {
} from '../fields/config/types.js'
export { getFieldPaths } from '../fields/getFieldPaths.js'
export * from '../fields/validations.js'
export type {
@@ -50,21 +49,21 @@ export type {
GetFolderDataResult,
Subfolder,
} from '../folders/types.js'
export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js'
export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js'
export { validOperators, validOperatorSet } from '../types/constants.js'
export { formatFilesize } from '../uploads/formatFilesize.js'
export { isImage } from '../uploads/isImage.js'
export { isImage } from '../uploads/isImage.js'
export { combineWhereConstraints } from '../utilities/combineWhereConstraints.js'
export {
deepCopyObject,
deepCopyObjectComplex,
deepCopyObjectSimple,
deepCopyObjectSimpleWithoutReactComponents,
} from '../utilities/deepCopyObject.js'
export {
deepMerge,
deepMergeWithCombinedArrays,
@@ -75,16 +74,18 @@ export {
export { extractID } from '../utilities/extractID.js'
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
export { flattenAllFields } from '../utilities/flattenAllFields.js'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
export { formatAdminURL } from '../utilities/formatAdminURL.js'
export { formatLabels, toWords } from '../utilities/formatLabels.js'
export { getBestFitFromSizes } from '../utilities/getBestFitFromSizes.js'
export { getDataByPath } from '../utilities/getDataByPath.js'
export { getFieldPermissions } from '../utilities/getFieldPermissions.js'
export { getSafeRedirect } from '../utilities/getSafeRedirect.js'
export { getSelectMode } from '../utilities/getSelectMode.js'
export { getSiblingData } from '../utilities/getSiblingData.js'

View File

@@ -8,7 +8,7 @@ describe('getSafeRedirect', () => {
'should allow safe relative path: %s',
(input) => {
// If the input is a clean relative path, it should be returned as-is
expect(getSafeRedirect(input, fallback)).toBe(input)
expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(input)
},
)
@@ -17,7 +17,7 @@ describe('getSafeRedirect', () => {
'should fallback on invalid or non-string input: %s',
(input) => {
// If the input is not a valid string, it should return the fallback
expect(getSafeRedirect(input as any, fallback)).toBe(fallback)
expect(getSafeRedirect({ redirectTo: input as any, fallbackTo: fallback })).toBe(fallback)
},
)
@@ -36,20 +36,24 @@ describe('getSafeRedirect', () => {
'%2Fjavascript:alert(1)', // encoded JavaScript scheme
])('should block unsafe redirect: %s', (input) => {
// All of these should return the fallback because theyre unsafe
expect(getSafeRedirect(input, fallback)).toBe(fallback)
expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(fallback)
})
// Input with extra spaces should still be properly handled
it('should trim whitespace before evaluating', () => {
// A valid path with surrounding spaces should still be accepted
expect(getSafeRedirect(' /dashboard ', fallback)).toBe('/dashboard')
expect(getSafeRedirect({ redirectTo: ' /dashboard ', fallbackTo: fallback })).toBe(
'/dashboard',
)
// An unsafe path with spaces should still be rejected
expect(getSafeRedirect(' //example.com ', fallback)).toBe(fallback)
expect(getSafeRedirect({ redirectTo: ' //example.com ', fallbackTo: fallback })).toBe(
fallback,
)
})
// If decoding the input fails (e.g., invalid percent encoding), it should not crash
it('should return fallback on invalid encoding', () => {
expect(getSafeRedirect('%E0%A4%A', fallback)).toBe(fallback)
expect(getSafeRedirect({ redirectTo: '%E0%A4%A', fallbackTo: fallback })).toBe(fallback)
})
})

View File

@@ -1,17 +1,22 @@
export const getSafeRedirect = (
redirectParam: string | string[],
fallback: string = '/',
): string => {
if (typeof redirectParam !== 'string') {
return fallback
export const getSafeRedirect = ({
allowAbsoluteUrls = false,
fallbackTo = '/',
redirectTo,
}: {
allowAbsoluteUrls?: boolean
fallbackTo?: string
redirectTo: string | string[]
}): string => {
if (typeof redirectTo !== 'string') {
return fallbackTo
}
// Normalize and decode the path
let redirectPath: string
try {
redirectPath = decodeURIComponent(redirectParam.trim())
redirectPath = decodeURIComponent(redirectTo.trim())
} catch {
return fallback // invalid encoding
return fallbackTo // invalid encoding
}
const isSafeRedirect =
@@ -30,5 +35,10 @@ export const getSafeRedirect = (
// Prevent attempts to redirect to full URLs using "/http:" or "/https:"
!redirectPath.toLowerCase().startsWith('/http')
return isSafeRedirect ? redirectPath : fallback
const isAbsoluteSafeRedirect =
allowAbsoluteUrls &&
// Must be a valid absolute URL with http or https
/^https?:\/\/\S+$/i.test(redirectPath)
return isSafeRedirect || isAbsoluteSafeRedirect ? redirectPath : fallbackTo
}