fix(audit-followup): close regressions/gaps from the persona-audit re-review
Adversarial re-review of the persona-audit commit surfaced two HIGH regressions I'd introduced plus MED/LOW gaps. All fixed: HIGH - Auth privilege escalation: reads are sessionStorage-first, but guest `setAuth` wrote only localStorage — a resident admin token shadowed a new guest login (guest ran as admin on a shared device). setAuth/setAdminAuth now DISPLACE the other store so exactly one identity is resident. Adds unit + e2e regression guards. - Host dashboard: `exportGenerating` used `||`, so a one-half export failure stuck on "wird erstellt…" forever and never showed the re-release hint. Changed to `&&`. MEDIUM - Locked-upload auto-resume was unreliable (`event-opened` has no reconnect replay and SSE is down on non-feed pages) — now also resumes on `feed-delta`, which fires on every SSE reconnect. - Queue-cap eviction could drop a recoverable locked item (parked as `error` with a live blob) — now evicts only `done`/`blocked`. - Host page async-`onMount` leaked SSE handlers if unmounted mid-load — added a `destroyed` guard before registration. LOW - An unparseable 403 body now keeps the blob (reversible) instead of purging it. - `refreshEventState` is sequence-guarded so a late close-refresh can't clobber a reopen. - `/export/status` moved out of the all-or-nothing `reload()` so its failure can't blank the whole host dashboard. Verified: svelte-check 0 errors, 36 frontend unit tests (incl. new displacement guards). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
51
e2e/specs/07-adversarial/admin-guest-displacement.spec.ts
Normal file
51
e2e/specs/07-adversarial/admin-guest-displacement.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Regression guard — privilege escalation via mixed auth storage.
|
||||
*
|
||||
* The admin JWT lives in sessionStorage; guest JWTs in localStorage; reads are
|
||||
* sessionStorage-first. So a guest login MUST displace any resident admin token — otherwise
|
||||
* a leftover admin session on a shared "event laptop" would shadow the guest's own token and
|
||||
* silently hand the next person who joins full admin rights. This exercises the real UI flow.
|
||||
*/
|
||||
import { test, expect } from '../../fixtures/test';
|
||||
import { JoinPage } from '../../page-objects';
|
||||
|
||||
function roleOf(page: import('@playwright/test').Page) {
|
||||
return page.evaluate(() => {
|
||||
const t = sessionStorage.getItem('eventsnap_jwt') ?? localStorage.getItem('eventsnap_jwt');
|
||||
if (!t) return null;
|
||||
try {
|
||||
return JSON.parse(atob(t.split('.')[1])).role;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('AuthZ — guest join displaces a resident admin session', () => {
|
||||
test('joining as a guest with an admin token resident in sessionStorage yields GUEST rights', async ({
|
||||
page,
|
||||
api,
|
||||
}) => {
|
||||
// Simulate an admin who logged in on a shared device and walked away without closing the
|
||||
// tab — their token sits in sessionStorage.
|
||||
const adminJwt = await api.adminLogin();
|
||||
await page.goto('/');
|
||||
await page.evaluate((jwt) => sessionStorage.setItem('eventsnap_jwt', jwt), adminJwt);
|
||||
|
||||
// A different person now joins as a guest in that same tab.
|
||||
const join = new JoinPage(page);
|
||||
await join.goto();
|
||||
await join.joinAs('DisplacementGuest');
|
||||
await join.continueToFeed();
|
||||
|
||||
// The effective identity must be the GUEST — not the leftover admin.
|
||||
expect(await roleOf(page)).toBe('guest');
|
||||
// The admin token must have been cleared from sessionStorage by the guest login.
|
||||
expect(await page.evaluate(() => sessionStorage.getItem('eventsnap_jwt'))).toBeNull();
|
||||
|
||||
// And the guest must NOT be able to reach the admin dashboard.
|
||||
await page.goto('/admin');
|
||||
await page.waitForURL(/admin\/login|join|feed/, { timeout: 5_000 });
|
||||
expect(page.url()).not.toMatch(/\/admin(\/)?$/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user