initodwehjowecfuihe
This commit is contained in:
@@ -194,11 +194,61 @@ export function deriveSigningKey(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeTimestamp(encoded: string): number {
|
/** Big-endian int from b64url timestamp chunk (без epoch). */
|
||||||
|
function decodeTimestampRaw(encoded: string): number {
|
||||||
const raw = b64urlDecode(encoded);
|
const raw = b64urlDecode(encoded);
|
||||||
let ts = 0;
|
let ts = 0;
|
||||||
for (const b of raw) ts = ts * 256 + b;
|
for (const b of raw) ts = ts * 256 + b;
|
||||||
return ts + ITSDANGEROUS_EPOCH;
|
return ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMESTAMP_SKEW_SEC = 60;
|
||||||
|
/** Ниже — отсекаем legacy raw, чтобы не путать с unix. */
|
||||||
|
const MIN_PLAUSIBLE_UNIX = 1_577_836_800;
|
||||||
|
|
||||||
|
type TimestampCheck = 'ok' | 'future' | 'expired';
|
||||||
|
|
||||||
|
function checkIssuedAt(issuedAt: number, maxAgeSec: number): TimestampCheck {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
if (issuedAt > now + TIMESTAMP_SKEW_SEC) return 'future';
|
||||||
|
if (now - issuedAt > maxAgeSec) return 'expired';
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* itsdangerous 2.x (prod auth): unix в payload.
|
||||||
|
* Старый URLSafeTimedSerializer / test-jwt-signer: raw + ITSDANGEROUS_EPOCH.
|
||||||
|
*/
|
||||||
|
function verifyCsrfTimestamp(
|
||||||
|
tsStr: string,
|
||||||
|
maxAgeSec: number,
|
||||||
|
): { ok: true; mode: 'unix' | 'legacy-epoch' } | { ok: false; reason: string } {
|
||||||
|
let raw: number;
|
||||||
|
try {
|
||||||
|
raw = decodeTimestampRaw(tsStr);
|
||||||
|
} catch {
|
||||||
|
return { ok: false, reason: 'Invalid timestamp' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates: { issuedAt: number; mode: 'unix' | 'legacy-epoch' }[] = [
|
||||||
|
{ issuedAt: raw, mode: 'unix' },
|
||||||
|
{ issuedAt: raw + ITSDANGEROUS_EPOCH, mode: 'legacy-epoch' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastReason = 'Invalid timestamp';
|
||||||
|
for (const { issuedAt, mode } of candidates) {
|
||||||
|
if (issuedAt < MIN_PLAUSIBLE_UNIX) continue;
|
||||||
|
const check = checkIssuedAt(issuedAt, maxAgeSec);
|
||||||
|
if (check === 'ok') {
|
||||||
|
if (mode === 'legacy-epoch') {
|
||||||
|
logger.warn('CSRF timestamp decoded as legacy-epoch (itsdangerous 1.x / test signer)');
|
||||||
|
}
|
||||||
|
return { ok: true, mode };
|
||||||
|
}
|
||||||
|
lastReason = check === 'future' ? 'Token from the future' : 'Token expired';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false, reason: lastReason };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CsrfVerifyResult {
|
export interface CsrfVerifyResult {
|
||||||
@@ -262,13 +312,9 @@ function verifyCsrfTokenWithParams(
|
|||||||
return { valid: false, reason: 'Signature mismatch' };
|
return { valid: false, reason: 'Signature mismatch' };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const tsResult = verifyCsrfTimestamp(tsStr, cfg.maxAgeSec);
|
||||||
const issuedAt = decodeTimestamp(tsStr);
|
if (!tsResult.ok) {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
return { valid: false, reason: tsResult.reason };
|
||||||
if (issuedAt > now + 60) return { valid: false, reason: 'Token from the future' };
|
|
||||||
if (now - issuedAt > cfg.maxAgeSec) return { valid: false, reason: 'Token expired' };
|
|
||||||
} catch {
|
|
||||||
return { valid: false, reason: 'Invalid timestamp' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
|
|||||||
Reference in New Issue
Block a user