Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions webapp/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,33 @@ function WorkspaceToTeamRedirect() {
return null
}

/**
* On initial load only: if URL ends with /error (and not the canonical /error page), redirect to path without it.
* Prevents infinite redirect loop from error boundary's relative URL redirect.
*/
function InitialLoadErrorPathFix() {
const location = useLocation()
const history = useHistory()
const hasRun = React.useRef(false)

useEffect(() => {
if (hasRun.current) return
hasRun.current = true

const pathname = location.pathname
if (pathname === '/error' || pathname === '/access-denied') return
if (!pathname.endsWith('/error')) return

let validPath = pathname
while (validPath.endsWith('/error') && validPath !== '/error') {
validPath = validPath.replace(/\/error$/, '') || '/'
}
history.replace(validPath + location.search)
Comment on lines +191 to +195
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize leaked basename suffixes, not just trailing /error.

At Line [193], the loop strips only /error. For corrupted URLs like /team/.../error/plugins/focalboard/error, this can stop at /team/.../error/plugins/focalboard, which is still invalid.

Proposed fix
     useEffect(() => {
         if (hasRun.current) return
         hasRun.current = true

         const pathname = location.pathname
+        const leakedBase = Utils.getFrontendBaseURL().replace(/^\/+|\/+$/g, '')
+        const escapedBase = leakedBase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+        const basePart = leakedBase ? `${escapedBase}/` : ''
+        const trailingErrorPattern = new RegExp(`/(?:${basePart})?error$`)
+
         if (pathname === '/error' || pathname === '/access-denied') return
-        if (!pathname.endsWith('/error')) return
+        if (!trailingErrorPattern.test(pathname)) return

         let validPath = pathname
-        while (validPath.endsWith('/error') && validPath !== '/error') {
-            validPath = validPath.replace(/\/error$/, '') || '/'
+        while (trailingErrorPattern.test(validPath) && validPath !== '/error') {
+            validPath = validPath.replace(trailingErrorPattern, '') || '/'
         }
         history.replace(validPath + location.search)
     }, [location.pathname, location.search, history])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/router.tsx` around lines 191 - 195, The current loop only strips
trailing "/error" segments (using validPath.endsWith('/error')), which fails for
leaked basename suffixes like "/team/.../error/plugins/focalboard/error"; change
the normalization in the router (variables: pathname, validPath,
history.replace) to repeatedly remove any "/error" segment and everything after
it instead of only trimming a trailing "/error"—e.g., while
validPath.includes('/error') and validPath !== '/error', set validPath =
validPath.slice(0, validPath.indexOf('/error')) || '/'—then call
history.replace(validPath + location.search) so all leaked "/error" basename
suffixes are normalized.

}, [location.pathname, location.search, history])

return null
}

function GlobalErrorRedirect() {
const globalError = useAppSelector<string>(getGlobalError)
const dispatch = useAppDispatch()
Expand Down Expand Up @@ -223,6 +250,7 @@ const FocalboardRouter = (props: Props): JSX.Element => {
return (
<Router history={browserHistory}>
<GlobalErrorRedirect/>
<InitialLoadErrorPathFix/>
<Switch>
<HomeToCurrentTeam
path='/'
Expand Down
Loading