fix: refuse an impersonation link when its use can't be recorded #1066

This commit is contained in:
Linus Rath
2026-10-31 22:53:34 +01:00
parent c20ecd4b1a
commit d28ee361c1
2 changed files with 16 additions and 6 deletions
@@ -1,5 +1,5 @@
// @vitest-environment node // @vitest-environment node
import { mkdtempSync, readdirSync, rmSync, utimesSync } from 'node:fs'; import { mkdtempSync, readdirSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -44,6 +44,14 @@ describe('consumeImpersonationJti', () => {
expect(await consumeImpersonationJti('jti-1', exp, now)).toBe(false); expect(await consumeImpersonationJti('jti-1', exp, now)).toBe(false);
}); });
it('refuses the token when its marker cannot be written', async () => {
// A state "dir" that is a file: the marker dir can't be created.
const blocked = path.join(stateDir, 'not-a-dir');
writeFileSync(blocked, '');
process.env.ADMIN_STATE_DIR = blocked;
expect(await consumeImpersonationJti('jti-unrecorded', exp, now)).toBe(false);
});
it('prunes markers once their token can no longer verify', async () => { it('prunes markers once their token can no longer verify', async () => {
await consumeImpersonationJti('jti-old', exp, now); await consumeImpersonationJti('jti-old', exp, now);
const dir = path.join(stateDir, 'impersonation-jti'); const dir = path.join(stateDir, 'impersonation-jti');
+7 -5
View File
@@ -20,7 +20,8 @@ async function pruneExpired(dir: string, nowMs: number): Promise<void> {
} }
/** /**
* Record an impersonation token's jti as used; false when it was used before. * Record an impersonation token's jti as used; false when it was used before
* or the use can't be recorded.
* *
* The in-process cache alone forgot every jti on restart and knew nothing of * The in-process cache alone forgot every jti on restart and knew nothing of
* other replicas, so a captured handoff link could be redeemed again within * other replicas, so a captured handoff link could be redeemed again within
@@ -45,11 +46,12 @@ export async function consumeImpersonationJti(
return true; return true;
} catch (err) { } catch (err) {
if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false; if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false;
// The state directory is documented as always writable; if it is not, // Without the marker a restart or another replica would accept the
// keep impersonation working on the in-process cache. // token again, so it is refused rather than trusted to this process's
logger.warn('Impersonation replay store unavailable, using the in-process cache only', { // memory.
logger.error('Impersonation replay store unavailable, refusing the token', {
error: err instanceof Error ? err.message : 'Unknown', error: err instanceof Error ? err.message : 'Unknown',
}); });
return true; return false;
} }
} }