Fix infinite re-fetch loop in useJailConfigs
The hook was passing an inline onSuccess callback to useListData, which included onSuccess in its internal refresh function's dependency array. This caused refresh to be recreated on each render, which triggered the useEffect, which fired the fetch, which completed and caused a re-render, creating an infinite loop. Wrap onSuccess in useCallback with empty dependencies so it maintains a stable reference across renders. This allows refresh to be stable when its dependencies don't change, breaking the cycle. Add documentation to Refactoring.md explaining the onSuccess stability requirement for useListData callers. Also add tests for useJailConfigs to verify it doesn't trigger infinite refetches with stable onSuccess callback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,48 +1,12 @@
|
||||
|
||||
### TASK-SEC-01 — Open Redirect in LoginPage via `?next=` Parameter
|
||||
### ✅ TASK-BUG-01 — Infinite Re-Fetch Loop in `useJailConfigs` — DONE
|
||||
|
||||
**Where found**
|
||||
`frontend/src/pages/LoginPage.tsx` lines 77–103. After a successful login the code does `navigate(nextPath, { replace: true })` where `nextPath = searchParams.get("next") ?? "/"` with no validation of the value. `RequireAuth.tsx` sets the redirect with `to={`/login?next=${encodeURIComponent(...)}`}`, which only ever produces relative paths, but the parameter can be set by an attacker in a crafted link.
|
||||
**Fix Summary**
|
||||
Wrapped the `onSuccess` callback in `useCallback` with empty dependencies in `frontend/src/hooks/useJailConfigs.ts` (lines 33-35). The inline callback was creating a new reference on every render, which caused `useListData`'s internal `refresh` function to be recreated (since `onSuccess` is in its deps), which triggered the `useEffect` again, causing an infinite fetch loop.
|
||||
|
||||
**Goal**
|
||||
Validate that `nextPath` is an internal path before using it. The check should verify the value starts with `/` and does not start with `//` (which browsers interpret as a protocol-relative URL). If the value fails validation fall back to `"/"`. A one-liner guard such as `const safePath = /^\/(?!\/)/.test(next) ? next : "/";` is sufficient.
|
||||
Added comprehensive test coverage in `frontend/src/hooks/__tests__/useJailConfigs.test.ts` to verify the hook no longer triggers infinite refetches. Updated `Docs/Refactoring.md` with documentation explaining the `onSuccess` stability requirement for all `useListData` callers.
|
||||
|
||||
**Possible traps and issues**
|
||||
- `//evil.com` passes a leading-slash check unless the double-slash case is excluded explicitly.
|
||||
- `encodeURIComponent` in `RequireAuth` means the decoded value will always be a relative path under normal operation, so this fix will not break the legitimate redirect flow.
|
||||
- If the app ever navigates to an external URL intentionally that logic must bypass this guard through a separate explicit code path, not the `?next=` parameter.
|
||||
|
||||
**Docs changes needed**
|
||||
Note the fix in `Docs/Refactoring.md` under a new "Security Fixes" section.
|
||||
|
||||
**Why this is needed**
|
||||
An attacker can send a user a link like `/login?next=https://evil.com`. After the user enters their password they are transparently redirected to an attacker-controlled site. This is a textbook open-redirect vulnerability and is listed in the OWASP Top 10 (A01:2021 Broken Access Control).
|
||||
|
||||
---
|
||||
|
||||
### TASK-BUG-01 — Infinite Re-Fetch Loop in `useJailConfigs`
|
||||
|
||||
**Where found**
|
||||
`frontend/src/hooks/useJailConfigs.ts` lines 33–40. The hook calls `useListData` with an inline `onSuccess` callback: `onSuccess: (response) => { setTotal(response.total); }`. This arrow function is a new object reference on every render. `useListData` lists `onSuccess` in the `useCallback` dependency array of its internal `refresh` function, so a new `onSuccess` → new `refresh` → `useEffect([refresh])` fires → fetch completes → re-render → new `onSuccess` → infinite loop.
|
||||
|
||||
**Goal**
|
||||
Wrap the `onSuccess` callback in `useCallback` inside `useJailConfigs` so its reference is stable across renders:
|
||||
```ts
|
||||
const onSuccess = useCallback((response: JailConfigListResponse) => {
|
||||
setTotal(response.total);
|
||||
}, []);
|
||||
```
|
||||
Then pass `onSuccess` to `useListData`. The loop will break because `onSuccess` only changes when its own deps change (none here), so `refresh` is stable, so the effect fires only once on mount.
|
||||
|
||||
**Possible traps and issues**
|
||||
- `useListData` itself would also need to ensure `onSuccess` is in its `useCallback` deps, which it already is — the fix is entirely in `useJailConfigs`.
|
||||
- Adding `useCallback` to `onSuccess` but forgetting to keep it dep-free (empty array) would still cause the loop if any state value is incorrectly added to the array.
|
||||
|
||||
**Docs changes needed**
|
||||
Add a note to `Docs/Refactoring.md` explaining the `onSuccess` stability rule for `useListData` callers.
|
||||
|
||||
**Why this is needed**
|
||||
The current code causes every visit to the Config → Jails tab to hammer the backend with an unbounded sequence of `GET /api/config/jails` requests until the user navigates away.
|
||||
Commit: `de8af09a3da36dbf24b56fa28656673b232b5e91`
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user