inithilyhb
This commit is contained in:
@@ -8,6 +8,8 @@ import { logger } from '../lib/logger';
|
|||||||
*
|
*
|
||||||
* Token format: <b64url_payload>.<b64url_timestamp>.<b64url_signature>
|
* Token format: <b64url_payload>.<b64url_timestamp>.<b64url_signature>
|
||||||
*
|
*
|
||||||
|
* Digests: sha1 (20-byte sig, legacy Flask-WTF), sha256 (32), sha512 (64, itsdangerous 2.x default).
|
||||||
|
*
|
||||||
* Default algorithm (itsdangerous ≥ 2.0):
|
* Default algorithm (itsdangerous ≥ 2.0):
|
||||||
* - digest: SHA-512 (HMAC)
|
* - digest: SHA-512 (HMAC)
|
||||||
* - salt: "itsdangerous.Signer" (or app-specific, e.g. "csrf-token")
|
* - salt: "itsdangerous.Signer" (or app-specific, e.g. "csrf-token")
|
||||||
@@ -155,7 +157,20 @@ export function generateCsrfToken(): { token: string; maxAgeSec: number } {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyCsrfTokenWithConfig(cfg: CsrfConfig, token: string): CsrfVerifyResult {
|
type CsrfDigest = 'sha1' | 'sha256' | 'sha512';
|
||||||
|
|
||||||
|
/** HMAC output length → digest (auth-service legacy часто sha1 → sigLen=20). */
|
||||||
|
const DIGEST_BY_SIG_LEN: Record<number, CsrfDigest> = {
|
||||||
|
20: 'sha1',
|
||||||
|
32: 'sha256',
|
||||||
|
64: 'sha512',
|
||||||
|
};
|
||||||
|
|
||||||
|
function verifyCsrfTokenWithDigest(
|
||||||
|
cfg: CsrfConfig,
|
||||||
|
digest: CsrfDigest,
|
||||||
|
token: string,
|
||||||
|
): CsrfVerifyResult {
|
||||||
if (!token || typeof token !== 'string') return { valid: false, reason: 'Empty token' };
|
if (!token || typeof token !== 'string') return { valid: false, reason: 'Empty token' };
|
||||||
|
|
||||||
const lastDot = token.lastIndexOf('.');
|
const lastDot = token.lastIndexOf('.');
|
||||||
@@ -169,8 +184,8 @@ function verifyCsrfTokenWithConfig(cfg: CsrfConfig, token: string): CsrfVerifyRe
|
|||||||
|
|
||||||
const tsStr = payloadTs.slice(prevDot + 1);
|
const tsStr = payloadTs.slice(prevDot + 1);
|
||||||
|
|
||||||
const derived = deriveKey(cfg.secret, cfg.salt, cfg.digest);
|
const derived = deriveKey(cfg.secret, cfg.salt, digest);
|
||||||
const expectedSig = crypto.createHmac(cfg.digest, derived).update(payloadTs).digest();
|
const expectedSig = crypto.createHmac(digest, derived).update(payloadTs).digest();
|
||||||
|
|
||||||
let actualSig: Buffer;
|
let actualSig: Buffer;
|
||||||
try {
|
try {
|
||||||
@@ -203,29 +218,48 @@ function verifyCsrfTokenWithConfig(cfg: CsrfConfig, token: string): CsrfVerifyRe
|
|||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function digestsToTry(primary: CsrfVerifyResult): CsrfDigest[] {
|
||||||
|
const order: CsrfDigest[] = [];
|
||||||
|
const add = (d: CsrfDigest) => {
|
||||||
|
if (!order.includes(d)) order.push(d);
|
||||||
|
};
|
||||||
|
|
||||||
|
add(current!.digest);
|
||||||
|
if (primary.actualSigLen !== undefined) {
|
||||||
|
const inferred = DIGEST_BY_SIG_LEN[primary.actualSigLen];
|
||||||
|
if (inferred) add(inferred);
|
||||||
|
}
|
||||||
|
add('sha1');
|
||||||
|
add('sha256');
|
||||||
|
add('sha512');
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify CSRF token. If Vault digest differs from auth-service (sha256 vs sha512),
|
* Verify CSRF token. Fallback по длине подписи: sha1 (20) / sha256 (32) / sha512 (64).
|
||||||
* retry once with the alternate digest — типичный случай Flask-WTF / itsdangerous 2.x.
|
|
||||||
*/
|
*/
|
||||||
export function verifyCsrfToken(token: string): CsrfVerifyResult {
|
export function verifyCsrfToken(token: string): CsrfVerifyResult {
|
||||||
if (!current) return { valid: false, reason: 'CSRF secret not loaded' };
|
if (!current) return { valid: false, reason: 'CSRF secret not loaded' };
|
||||||
|
|
||||||
const primary = verifyCsrfTokenWithConfig(current, token);
|
const primaryDigest = current.digest;
|
||||||
|
const primary = verifyCsrfTokenWithDigest(current, primaryDigest, token);
|
||||||
if (primary.valid) return primary;
|
if (primary.valid) return primary;
|
||||||
|
|
||||||
if (primary.reason !== 'Signature length mismatch') {
|
if (primary.reason !== 'Signature length mismatch' && primary.reason !== 'Signature mismatch') {
|
||||||
return primary;
|
return primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
const altDigest: 'sha256' | 'sha512' = current.digest === 'sha256' ? 'sha512' : 'sha256';
|
for (const digest of digestsToTry(primary)) {
|
||||||
const fallback = verifyCsrfTokenWithConfig({ ...current, digest: altDigest }, token);
|
if (digest === primaryDigest) continue;
|
||||||
if (fallback.valid) {
|
const attempt = verifyCsrfTokenWithDigest(current, digest, token);
|
||||||
|
if (attempt.valid) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`CSRF verified with fallback digest ${altDigest} (Vault digest=${current.digest}). ` +
|
`CSRF verified with fallback digest ${digest} (Vault digest=${primaryDigest}, ` +
|
||||||
'Align auth-service URLSafeTimedSerializer digest_method with Vault `digest` field.',
|
`sigLen=${primary.actualSigLen ?? '?'}). Align auth digest_method with Vault.`,
|
||||||
);
|
);
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return primary;
|
return primary;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user