Description
Two independent defects in src/context/AuthContext.jsx:
- A single malformed character in
sessionStorage makes the entire application fail to mount,
with no way for the user to recover from the UI — because AuthProvider sits outside
ErrorBoundary.
- Server-side session revocation is detected and then silently ignored. The code that notices
it contains a comment describing the action it does not take.
Problem 1: a corrupt sessionStorage value bricks the app permanently
const [user, setUser] = useState(() => {
const savedUser = sessionStorage.getItem("medtrack_user");
if (savedUser) return JSON.parse(savedUser); // no try/catch
return null;
});
const [authorityState, setAuthorityState] = useState(() => {
const savedAuth = sessionStorage.getItem("medtrack_authority");
return savedAuth ? JSON.parse(savedAuth) : { authorityVersion: 1, permissions: [] };
});
JSON.parse throws on any invalid value. Thrown from a useState initializer, it propagates out of
AuthProvider's render.
The reason this is not merely an ugly error is the provider order in src/App.jsx:
<AuthProvider> {/* throws here */}
<ThemeProvider>
<ErrorBoundary> {/* never mounts, so it cannot catch anything */}
<AppContent />
ErrorBoundary only catches errors from its own subtree. Because it is nested inside the provider
that throws, it never mounts, and React unmounts the whole tree — a permanently blank page.
The user cannot recover, because every route is inside the same crashed tree: there is no page left
that could offer a "sign out" button, and sessionStorage is not something a non-technical user can
clear. The only way out is devtools or a different browser profile.
Reproduce:
sessionStorage.setItem("medtrack_user", "{oops");
location.reload(); // blank page, Unexpected token o in JSON at position 1
This is reachable without an attacker: an interrupted write, a quota error mid-write, or any future
change to the stored shape produces it.
Problem 2: detected session revocation is ignored
// If backend authority version has changed and active sessions were revoked
if (authorityState.authorityVersion && data.authorityVersion > authorityState.authorityVersion) {
console.warn("Authority version mismatch detected. Session revoked by administrator.");
// Trigger logout if authority version was bumped on server
}
The branch logs and returns. logout() is never called, and the comment on the last line describes
the missing behaviour.
Authority version exists specifically so an administrator can revoke live sessions —
POST /api/auth/authority/version/increment and /bump-global are wired for exactly that, and
EnterpriseSecurityCenter.jsx presents it to admins as "Increment Authority Version … Active tokens
invalidated!". The client polls every 60 seconds, notices the bump, and carries on as if nothing
happened. The UI keeps rendering with the old permissions until the JWT expires on its own.
The backend does reject the stale token on the next API call, so this is not a full authorization
bypass — but the console the product presents as an emergency revocation control does not visibly do
anything, which is worse than not offering it.
Problem 3: the polling effect tears itself down repeatedly
const fetchUserAuthority = useCallback(async (userId) => {
...
setAuthorityState(newAuth);
}, [authorityState.authorityVersion]);
useEffect(() => {
if (user && user.id) {
fetchUserAuthority(user.id);
const interval = setInterval(() => fetchUserAuthority(user.id), 60000);
return () => clearInterval(interval);
}
}, [user?.id, fetchUserAuthority]);
fetchUserAuthority depends on authorityState.authorityVersion and also sets authorityState.
So every time the version changes, the callback identity changes, the effect re-runs, the interval is
cleared and recreated, and an immediate extra fetchUserAuthority fires outside the 60-second
cadence. The comparison in Problem 2 also reads authorityState captured at callback-creation time,
so it is comparing against a value that may already be stale.
Expected Result
- A corrupt or tampered
sessionStorage value is discarded and the app boots signed-out.
- An authority-version bump signs the user out and returns them to the login page with an
explanation.
- The 60-second poll runs on a stable interval.
Proposed Fix
- Add a small
safeSessionStorage helper that returns a fallback when JSON.parse throws (and when
sessionStorage is unavailable at all — Safari private mode throws on access), and clears the bad
key so the next load is clean.
- Move
ErrorBoundary to the outside of the provider stack in App.jsx, so a throw in any provider
is catchable rather than fatal.
- Call
logout() on a detected version bump and surface the reason on the login screen.
- Compare against the previous version using the state-updater form so the check does not read a
stale capture, and drop authorityState.authorityVersion from the callback's dependencies so the
interval is stable.
Labels
bug, frontend
Description
Two independent defects in
src/context/AuthContext.jsx:sessionStoragemakes the entire application fail to mount,with no way for the user to recover from the UI — because
AuthProvidersits outsideErrorBoundary.it contains a comment describing the action it does not take.
Problem 1: a corrupt sessionStorage value bricks the app permanently
JSON.parsethrows on any invalid value. Thrown from auseStateinitializer, it propagates out ofAuthProvider's render.The reason this is not merely an ugly error is the provider order in
src/App.jsx:ErrorBoundaryonly catches errors from its own subtree. Because it is nested inside the providerthat throws, it never mounts, and React unmounts the whole tree — a permanently blank page.
The user cannot recover, because every route is inside the same crashed tree: there is no page left
that could offer a "sign out" button, and
sessionStorageis not something a non-technical user canclear. The only way out is devtools or a different browser profile.
Reproduce:
This is reachable without an attacker: an interrupted write, a quota error mid-write, or any future
change to the stored shape produces it.
Problem 2: detected session revocation is ignored
The branch logs and returns.
logout()is never called, and the comment on the last line describesthe missing behaviour.
Authority version exists specifically so an administrator can revoke live sessions —
POST /api/auth/authority/version/incrementand/bump-globalare wired for exactly that, andEnterpriseSecurityCenter.jsxpresents it to admins as "Increment Authority Version … Active tokensinvalidated!". The client polls every 60 seconds, notices the bump, and carries on as if nothing
happened. The UI keeps rendering with the old permissions until the JWT expires on its own.
The backend does reject the stale token on the next API call, so this is not a full authorization
bypass — but the console the product presents as an emergency revocation control does not visibly do
anything, which is worse than not offering it.
Problem 3: the polling effect tears itself down repeatedly
fetchUserAuthoritydepends onauthorityState.authorityVersionand also setsauthorityState.So every time the version changes, the callback identity changes, the effect re-runs, the interval is
cleared and recreated, and an immediate extra
fetchUserAuthorityfires outside the 60-secondcadence. The comparison in Problem 2 also reads
authorityStatecaptured at callback-creation time,so it is comparing against a value that may already be stale.
Expected Result
sessionStoragevalue is discarded and the app boots signed-out.explanation.
Proposed Fix
safeSessionStoragehelper that returns a fallback whenJSON.parsethrows (and whensessionStorageis unavailable at all — Safari private mode throws on access), and clears the badkey so the next load is clean.
ErrorBoundaryto the outside of the provider stack inApp.jsx, so a throw in any provideris catchable rather than fatal.
logout()on a detected version bump and surface the reason on the login screen.stale capture, and drop
authorityState.authorityVersionfrom the callback's dependencies so theinterval is stable.
Labels
bug,frontend