Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | 18x 18x 18x 20x 15x 13x 100x 10x 8x 8x 8x 7x 7x 4x 4x 4x 4x 18x | 'use strict';
/**
* SHY-0060 — client age-claim tamper detection (AC60 / BDD143).
*
* A client may assert its own age in a request (`claimedAge`). That claim NEVER
* feeds the age gate — the gate always uses the server's DOB-derived age, so a
* client cannot unlock anything by lying. This module is the abuse signal: when
* the asserted age materially disagrees with the server's record, we 403 the
* request and fire a T&S alert.
*
* A ±1-year tolerance absorbs client/server timezone + birthday-boundary drift
* (client and server derive from the same DOB), so only egregious lies trip it.
*/
const { isAgeGatingEnabled } = require('./age-gating-flag');
const { ageFromDob } = require('./feature-access');
const TAMPER_TOLERANCE_YEARS = 1;
/**
* Pure predicate: does the client's asserted age materially disagree with the
* server's derived age? Non-numeric inputs never trip (no assertion, or no
* record to compare against).
*
* @param {unknown} claimedAge the client's asserted age
* @param {unknown} serverAge the server's DOB-derived age
* @returns {boolean}
*/
function isAgeClaimTampered(claimedAge, serverAge) {
if (typeof claimedAge !== 'number' || !Number.isFinite(claimedAge)) return false;
if (typeof serverAge !== 'number' || !Number.isFinite(serverAge)) return false;
return Math.abs(claimedAge - serverAge) > TAMPER_TOLERANCE_YEARS;
}
/**
* Detect a tampered age claim and, if found, fire a T&S alert + return a 403
* block. Returns null (no action) when: the client asserted no age; the
* operator flag is OFF; there is no DOB on record to compare against; or the
* claim is within tolerance. The alert is AWAITED (rare, security-critical
* path) but wrapped so it can never throw into the request.
*
* @param {FirebaseFirestore.Firestore} db
* @param {object} p
* @param {unknown} p.claimedAge the client's asserted age (from the request)
* @param {object|(() => Promise<object>)} p.userDataOrLoader acting user's doc,
* or a lazy loader invoked only when a claim is present AND the flag is ON
* @param {number} [p.nowMs=Date.now()] clock for the server age
* @returns {Promise<null | { status: number, body: object }>}
*/
async function checkAgeClaimTamper(db, { claimedAge, userDataOrLoader, nowMs = Date.now() }) {
// Cheap exits first: no assertion → nothing to check; flag OFF → engine inert.
if (typeof claimedAge !== 'number' || !Number.isFinite(claimedAge)) return null;
if (!(await isAgeGatingEnabled(db))) return null;
const userData =
typeof userDataOrLoader === 'function' ? await userDataOrLoader() : userDataOrLoader;
const dob = userData?.dateOfBirth;
if (typeof dob !== 'number' || !Number.isFinite(dob)) return null;
const serverAge = ageFromDob(dob, nowMs);
if (!isAgeClaimTampered(claimedAge, serverAge)) return null;
// Lazy-required so the pure predicate above stays importable without booting
// firebase (keeps the unit test infrastructure-free); require() caches, so
// this costs nothing after the first call.
const alertManager = require('../utils/alertManagerInstance');
try {
await alertManager.createAlert(
'AGE_CLAIM_TAMPER',
'critical',
'Age claim tamper detected',
`Client claimed age ${claimedAge}; server record derives ${serverAge}.`,
{ userId: userData?.uniqueId ?? null, claimedAge, serverAge },
);
} catch {
// The alert is best-effort; a failed write must never mask the 403.
}
return { status: 403, body: { error: 'Age verification failed.', errorId: 'AGE_CLAIM_TAMPER' } };
}
module.exports = { isAgeClaimTampered, checkAgeClaimTamper, TAMPER_TOLERANCE_YEARS };
|