Skip to content

[BUG] A corrupt sessionStorage value permanently bricks the app, and detected session revocation is never acted on #592

Description

@MOHITKOURAV01

Description

Two independent defects in src/context/AuthContext.jsx:

  1. 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.
  2. 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

Metadata

Metadata

Assignees

Labels

ECSoC26ECSoC 2026 InitiativeFrontendFrontend related changesbackendBackend related changesbugSomething isn't workingenhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions