mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-16 17:46:08 +00:00
46cac582ab
On a 403 to an unsafe method the client cleared its cached CSRF token and called ensureCsrfToken to retry. But ensureCsrfToken prefers the <meta name="csrf-token"> tag baked into the page, which the production panel always injects, so the "refresh" re-read the same stale token and the /csrf-token refetch was never reached — the retry re-sent the token that had just been rejected and the save failed with an error toast. The token lives in the session and rotates when the session is regenerated (for example re-login in another tab), leaving the tab's baked-in meta token stale. Fetch the current token straight from /csrf-token in the 403 branch so the retry uses the authoritative server value. The existing tests only passed because they strip the meta tag; the new test keeps a stale tag present.
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { http, HttpResponse } from 'msw';
|
|
|
|
import { httpRequest, setupHttp } from '@/api/http-init';
|
|
|
|
import { CSRF_TOKEN } from './msw/handlers';
|
|
import { server } from './msw/server';
|
|
|
|
const ORIGIN = 'http://localhost';
|
|
|
|
describe('httpRequest against the MSW-mocked network', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal('document', { querySelector: () => null });
|
|
window.X_UI_BASE_PATH = ORIGIN;
|
|
setupHttp();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
delete window.X_UI_BASE_PATH;
|
|
});
|
|
|
|
it('fetches a CSRF token, then refetches and retries once after a 403', async () => {
|
|
let posts = 0;
|
|
const sentTokens: (string | null)[] = [];
|
|
server.use(
|
|
http.get(`${ORIGIN}/csrf-token`, () => HttpResponse.json({ success: true, obj: CSRF_TOKEN })),
|
|
http.post(`${ORIGIN}/panel/api/test`, ({ request }) => {
|
|
posts += 1;
|
|
sentTokens.push(request.headers.get('X-CSRF-Token'));
|
|
if (posts === 1) return new HttpResponse(null, { status: 403 });
|
|
return HttpResponse.json({ success: true, obj: 'saved' });
|
|
}),
|
|
);
|
|
|
|
const res = await httpRequest('POST', '/panel/api/test', { hello: 'world' });
|
|
|
|
expect(posts).toBe(2);
|
|
expect(sentTokens).toEqual([CSRF_TOKEN, CSRF_TOKEN]);
|
|
expect(res.status).toBe(200);
|
|
expect(res.data).toEqual({ success: true, obj: 'saved' });
|
|
});
|
|
|
|
it('on a 403 refetches a fresh token from the server even when a stale meta tag is present', async () => {
|
|
const STALE = 'stale-meta-token';
|
|
const FRESH = 'fresh-server-token';
|
|
vi.stubGlobal('document', {
|
|
querySelector: (selector: string) =>
|
|
selector === 'meta[name="csrf-token"]' ? { getAttribute: () => STALE } : null,
|
|
});
|
|
setupHttp();
|
|
|
|
let posts = 0;
|
|
const sentTokens: (string | null)[] = [];
|
|
server.use(
|
|
http.get(`${ORIGIN}/csrf-token`, () => HttpResponse.json({ success: true, obj: FRESH })),
|
|
http.post(`${ORIGIN}/panel/api/test`, ({ request }) => {
|
|
posts += 1;
|
|
const token = request.headers.get('X-CSRF-Token');
|
|
sentTokens.push(token);
|
|
if (token !== FRESH) return new HttpResponse(null, { status: 403 });
|
|
return HttpResponse.json({ success: true, obj: 'saved' });
|
|
}),
|
|
);
|
|
|
|
const res = await httpRequest('POST', '/panel/api/test', { hello: 'world' });
|
|
|
|
expect(sentTokens).toEqual([STALE, FRESH]);
|
|
expect(posts).toBe(2);
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('resolves a safe GET without requesting a CSRF token', async () => {
|
|
let csrfHits = 0;
|
|
server.use(
|
|
http.get(`${ORIGIN}/csrf-token`, () => {
|
|
csrfHits += 1;
|
|
return HttpResponse.json({ success: true, obj: CSRF_TOKEN });
|
|
}),
|
|
http.get(`${ORIGIN}/panel/api/status`, () => HttpResponse.json({ success: true, obj: { up: true } })),
|
|
);
|
|
|
|
const res = await httpRequest('GET', '/panel/api/status');
|
|
|
|
expect(csrfHits).toBe(0);
|
|
expect(res.status).toBe(200);
|
|
expect(res.data).toEqual({ success: true, obj: { up: true } });
|
|
});
|
|
});
|