You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The panel magic-link poll (LoginService.startAuthPolling / GET {api-url}/auth/magic-link/poll?playerUuid=...) is scoped only by playerUuid, and its response carries no field identifying which magic-link request completed. This module tracks a local requestId per pending request and threads it through its own callbacks (handlePanelPollCompleted, completePanelLogin), but that id never round-trips through the UltiPanel worker -- the worker is never asked about a specific request, only "is there a completed status for this player".
Reproduction (logical, not yet exploited on a live worker)
Player runs /panel. Local request A is created and published; startAuthPolling begins polling for player-scoped completion, tracking request A's id locally.
An admin runs a credential-changing action (resetPassword, changePassword, unregister) on the same player. invalidateSession bumps the local invalidation generation and cancels request A's local bookkeeping (pendingPanelRequests entry + polling task) via cancelPendingPanelRequest. The remote magic link the worker issued for request A is not told to stop being valid -- there is no worker-side revoke call in this flow.
The player runs /panel again. Local request B is created and published; a new poll starts, tracking request B's id locally.
If the still-live remote link for request A is completed (e.g. a stale browser tab, an already-opened magic-link email/notification, or simple race with however long the worker keeps A pending), request B's poll -- which only ever asks the worker "is anything completed for this playerUuid" -- observes that completion and calls completePanelLogin(B, ...), authenticating through B's local bookkeeping even though the underlying confirmation was actually A's.
Why this module cannot close it alone
Every prior round of hardening on PR #18 (generation fencing before publish, atomic check-and-insert under a per-player lock, a post-POST re-check, isPanelRequestCurrent on the main-thread result-delivery callback, and request-id-keyed localpendingPanelRequests lookups in handlePanelPollCompleted) closes races in this module's own bookkeeping between a local invalidation and a local publish/poll/callback. None of them can close this specific gap, because it is not a local race -- it is that the remote poll response itself carries no way to distinguish which request completed. Closing it requires changes on the UltiPanel worker side; see the companion issue in UltiKits/ultipanel-api-worker.
Proposed direction (for this repository, once the worker side lands)
Once the worker returns and accepts a per-request id/nonce:
requestPanelLink's create call already generates and sends a requestId -- keep doing so, but expect the worker to echo it back (or accept it) on poll/completion.
startAuthPolling should scope its poll by that id (query param or request body), not just playerUuid.
handlePanelPollCompleted/completePanelLogin should verify the returned id matches the one this poll was started for before granting anything -- current local-id checks would then also be validated against the remote response, not just against this module's own map.
Related local hardening: LoginService.invalidateSession, requestPanelLink(Player, long), isPanelRequestCurrent, handlePanelPollCompleted -- see LoginService.java in this repository for the full history of local-race fixes this gap sits behind.
Summary
The panel magic-link poll (
LoginService.startAuthPolling/GET {api-url}/auth/magic-link/poll?playerUuid=...) is scoped only byplayerUuid, and its response carries no field identifying which magic-link request completed. This module tracks a localrequestIdper pending request and threads it through its own callbacks (handlePanelPollCompleted,completePanelLogin), but that id never round-trips through the UltiPanel worker -- the worker is never asked about a specific request, only "is there a completed status for this player".Reproduction (logical, not yet exploited on a live worker)
/panel. Local request A is created and published;startAuthPollingbegins polling for player-scoped completion, tracking request A's id locally.resetPassword,changePassword,unregister) on the same player.invalidateSessionbumps the local invalidation generation and cancels request A's local bookkeeping (pendingPanelRequestsentry + polling task) viacancelPendingPanelRequest. The remote magic link the worker issued for request A is not told to stop being valid -- there is no worker-side revoke call in this flow./panelagain. Local request B is created and published; a new poll starts, tracking request B's id locally.completePanelLogin(B, ...), authenticating through B's local bookkeeping even though the underlying confirmation was actually A's.Why this module cannot close it alone
Every prior round of hardening on PR #18 (generation fencing before publish, atomic check-and-insert under a per-player lock, a post-POST re-check,
isPanelRequestCurrenton the main-thread result-delivery callback, and request-id-keyed localpendingPanelRequestslookups inhandlePanelPollCompleted) closes races in this module's own bookkeeping between a local invalidation and a local publish/poll/callback. None of them can close this specific gap, because it is not a local race -- it is that the remote poll response itself carries no way to distinguish which request completed. Closing it requires changes on the UltiPanel worker side; see the companion issue inUltiKits/ultipanel-api-worker.Proposed direction (for this repository, once the worker side lands)
Once the worker returns and accepts a per-request id/nonce:
requestPanelLink's create call already generates and sends arequestId-- keep doing so, but expect the worker to echo it back (or accept it) on poll/completion.startAuthPollingshould scope its poll by that id (query param or request body), not justplayerUuid.handlePanelPollCompleted/completePanelLoginshould verify the returned id matches the one this poll was started for before granting anything -- current local-id checks would then also be validated against the remote response, not just against this module's own map.Context
LoginService.invalidateSession,requestPanelLink(Player, long),isPanelRequestCurrent,handlePanelPollCompleted-- seeLoginService.javain this repository for the full history of local-race fixes this gap sits behind.