mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-15 17:16:07 +00:00
fix(frontend): refetch a fresh CSRF token on 403 instead of reusing the stale meta tag
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.
This commit is contained in:
@@ -152,8 +152,11 @@ export async function httpRequest(
|
||||
|
||||
if (res.status === 403 && !SAFE_METHODS.has(method.toUpperCase())) {
|
||||
csrfToken = null;
|
||||
const fresh = await ensureCsrfToken();
|
||||
if (fresh) res = await performFetch(method, url, data, options, fresh);
|
||||
const fresh = await fetchCsrfToken();
|
||||
if (fresh) {
|
||||
csrfToken = fresh;
|
||||
res = await performFetch(method, url, data, options, fresh);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
|
||||
@@ -41,6 +41,35 @@ describe('httpRequest against the MSW-mocked network', () => {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user