@blamejs/blamejs-shop 0.4.74 → 0.4.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +48 -44
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +23 -2
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +5 -1
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-vendored-deps.js +6 -1
- package/lib/vendor/blamejs/examples/wiki/test/codebase-patterns.test.js +8 -4
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/auth/ciba.js +32 -8
- package/lib/vendor/blamejs/lib/auth/dpop.js +9 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +25 -12
- package/lib/vendor/blamejs/lib/auth/jwt.js +19 -3
- package/lib/vendor/blamejs/lib/auth/oauth.js +8 -2
- package/lib/vendor/blamejs/lib/auth/saml.js +19 -9
- package/lib/vendor/blamejs/lib/crypto-field.js +19 -1
- package/lib/vendor/blamejs/lib/csp.js +9 -0
- package/lib/vendor/blamejs/lib/db-query.js +33 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +24 -1
- package/lib/vendor/blamejs/lib/mail-dkim.js +20 -7
- package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +39 -5
- package/lib/vendor/blamejs/lib/pipl-cn.js +11 -8
- package/lib/vendor/blamejs/lib/request-helpers.js +146 -13
- package/lib/vendor/blamejs/lib/safe-json.js +26 -0
- package/lib/vendor/blamejs/lib/session.js +35 -117
- package/lib/vendor/blamejs/lib/sql.js +22 -0
- package/lib/vendor/blamejs/lib/ws-client.js +26 -0
- package/lib/vendor/blamejs/lib/x509-chain.js +71 -24
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.15.json +73 -0
- package/lib/vendor/blamejs/test/00-primitives.js +54 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ciba-authreqid-binding.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-builder.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +40 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-compose-pipeline.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/x509-chain-ca-enforcement.test.js +11 -0
- package/package.json +1 -1
|
@@ -469,12 +469,16 @@ function testNoHandrolledDeepClone() {
|
|
|
469
469
|
|
|
470
470
|
function testNoBareErrorThrows() {
|
|
471
471
|
// class: bare-error-throw
|
|
472
|
-
// Wiki
|
|
473
|
-
// error contract
|
|
472
|
+
// Wiki code that throws a plain Error skips the framework's structured
|
|
473
|
+
// error contract (a typed FrameworkError carries a stable .code a caller
|
|
474
|
+
// can dispatch on). This is a hard gate: use b.frameworkError.defineClass
|
|
475
|
+
// (see lib/build-app.js BuildAppError / lib/harvest-vendored-deps.js
|
|
476
|
+
// HarvestError), or add an `// allow:bare-error-throw` marker with a reason
|
|
477
|
+
// for a genuinely throwaway script throw.
|
|
474
478
|
var matches = _scan(/\bthrow\s+new\s+Error\s*\(/);
|
|
475
479
|
matches = _filterMarkers(matches, "bare-error-throw");
|
|
476
|
-
|
|
477
|
-
"(b.
|
|
480
|
+
_report("throw new Error(string) → use a typed error " +
|
|
481
|
+
"(b.frameworkError.defineClass) so callers can dispatch on .code",
|
|
478
482
|
matches);
|
|
479
483
|
}
|
|
480
484
|
|
|
@@ -400,6 +400,7 @@ var authBotChallenge = require("./lib/auth-bot-challenge");
|
|
|
400
400
|
var sessionDeviceBinding = require("./lib/session-device-binding");
|
|
401
401
|
var acme = require("./lib/acme");
|
|
402
402
|
var cert = require("./lib/cert");
|
|
403
|
+
var x509Chain = require("./lib/x509-chain");
|
|
403
404
|
var watcher = require("./lib/watcher");
|
|
404
405
|
var localDbThin = require("./lib/local-db-thin");
|
|
405
406
|
var daemon = require("./lib/daemon");
|
|
@@ -750,6 +751,7 @@ module.exports = {
|
|
|
750
751
|
sessionDeviceBinding: sessionDeviceBinding,
|
|
751
752
|
acme: acme,
|
|
752
753
|
cert: cert,
|
|
754
|
+
x509Chain: x509Chain,
|
|
753
755
|
ntpCheck: ntpCheck,
|
|
754
756
|
tlsExporter: tlsExporter,
|
|
755
757
|
watcher: watcher,
|
|
@@ -162,6 +162,10 @@ function create(opts) {
|
|
|
162
162
|
tokenEndpoint: opts.tokenEndpoint,
|
|
163
163
|
httpClientOpts: opts.httpClientOpts,
|
|
164
164
|
allowHttp: opts.allowHttp === true,
|
|
165
|
+
// Thread the SSRF opt-in through to the inner client's discovery / JWKS /
|
|
166
|
+
// token fetches — an operator with an internal-network IdP needs it, and
|
|
167
|
+
// dropping it silently made allowInternal an accepted-but-ignored option.
|
|
168
|
+
allowInternal: opts.allowInternal,
|
|
165
169
|
isOidc: true,
|
|
166
170
|
});
|
|
167
171
|
|
|
@@ -456,10 +460,11 @@ function create(opts) {
|
|
|
456
460
|
// returning its verified claims, or null when no id_token was supplied. A
|
|
457
461
|
// present-but-invalid id_token throws (fail-closed) so a forged token's
|
|
458
462
|
// sub/acr/amr can never be returned as trusted.
|
|
459
|
-
async function _verifyIdTokenIfPresent(idToken, auditKey, op) {
|
|
463
|
+
async function _verifyIdTokenIfPresent(idToken, auditKey, op, expectedAuthReqId) {
|
|
460
464
|
if (!idToken) return null;
|
|
465
|
+
var verified;
|
|
461
466
|
try {
|
|
462
|
-
|
|
467
|
+
verified = await inner.verifyIdToken(idToken);
|
|
463
468
|
} catch (e) {
|
|
464
469
|
_emitAudit("id_token_verify_fail", "failure",
|
|
465
470
|
auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
|
|
@@ -467,6 +472,27 @@ function create(opts) {
|
|
|
467
472
|
"ciba." + op + ": id_token failed verification: " +
|
|
468
473
|
((e && e.code) || (e && e.message) || String(e)));
|
|
469
474
|
}
|
|
475
|
+
// OpenID CIBA Core §7.3 / §10.1: the ID Token MUST carry the
|
|
476
|
+
// urn:openid:params:jwt:claim:auth_req_id claim and the RP MUST verify it
|
|
477
|
+
// equals the auth_req_id this flow used. Without it, an id_token minted for
|
|
478
|
+
// a DIFFERENT auth_req_id (another user's CIBA flow at the same RP) could be
|
|
479
|
+
// substituted — cross-user token substitution. verifyIdToken returns
|
|
480
|
+
// { header, claims }, so the claim lives on `.claims`. Constant-time compare.
|
|
481
|
+
// idToken is present (we returned early if not), so the binding is
|
|
482
|
+
// MANDATORY and ALWAYS checked — never gated behind a truthiness test that
|
|
483
|
+
// an empty string could slip. Both call sites pass a non-empty auth_req_id
|
|
484
|
+
// (validated at the parseNotification / pollToken entry), so a falsy value
|
|
485
|
+
// can't reach here to skip the comparison.
|
|
486
|
+
var payload = verified && verified.claims;
|
|
487
|
+
var boundId = payload && payload["urn:openid:params:jwt:claim:auth_req_id"];
|
|
488
|
+
if (typeof boundId !== "string" || !timingSafeEqual(boundId, expectedAuthReqId)) {
|
|
489
|
+
_emitAudit("id_token_authreqid_mismatch", "failure",
|
|
490
|
+
auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
|
|
491
|
+
throw new AuthError("auth-ciba/id-token-authreqid-mismatch",
|
|
492
|
+
"ciba." + op + ": id_token urn:openid:params:jwt:claim:auth_req_id does not " +
|
|
493
|
+
"match the expected auth_req_id (cross-user token-substitution defense)");
|
|
494
|
+
}
|
|
495
|
+
return verified;
|
|
470
496
|
}
|
|
471
497
|
|
|
472
498
|
async function pollToken(popts) {
|
|
@@ -536,7 +562,7 @@ function create(opts) {
|
|
|
536
562
|
// accepted-algorithms and enforces signature + iss/aud/exp. Returning an
|
|
537
563
|
// unverified id_token let a forged token's sub/acr/amr be trusted.
|
|
538
564
|
var pollClaims = await _verifyIdTokenIfPresent(rv.id_token,
|
|
539
|
-
"auth-ciba:" + popts.authReqId, "pollToken");
|
|
565
|
+
"auth-ciba:" + popts.authReqId, "pollToken", popts.authReqId);
|
|
540
566
|
_emitAudit("token_received", "success", {
|
|
541
567
|
authReqIdHash: sha3Hash("auth-ciba:" + popts.authReqId),
|
|
542
568
|
});
|
|
@@ -631,10 +657,8 @@ function create(opts) {
|
|
|
631
657
|
throw new AuthError("auth-ciba/no-notification-body",
|
|
632
658
|
"ciba.parseNotification: body required (Buffer/string parsed by middleware)");
|
|
633
659
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
"ciba.parseNotification: body missing auth_req_id");
|
|
637
|
-
}
|
|
660
|
+
validateOpts.requireNonEmptyString(body.auth_req_id,
|
|
661
|
+
"ciba.parseNotification: auth_req_id", AuthError, "auth-ciba/no-auth-req-id-in-body");
|
|
638
662
|
// Verify the pushed ID token (CIBA §10.2 / OIDC Core MUST) via the
|
|
639
663
|
// composed inner OAuth client before returning it as trusted. The
|
|
640
664
|
// notification-token bearer authenticates the CALLER, not the token; a
|
|
@@ -642,7 +666,7 @@ function create(opts) {
|
|
|
642
666
|
// arbitrary id_token claims the RP would trust on the documented
|
|
643
667
|
// "no follow-up call" push path.
|
|
644
668
|
var pushClaims = await _verifyIdTokenIfPresent(body.id_token,
|
|
645
|
-
"auth-ciba:" + body.auth_req_id, "parseNotification");
|
|
669
|
+
"auth-ciba:" + body.auth_req_id, "parseNotification", body.auth_req_id);
|
|
646
670
|
_emitAudit("notification_received", "success", {
|
|
647
671
|
authReqIdHash: sha3Hash("auth-ciba:" + body.auth_req_id),
|
|
648
672
|
mode: deliveryMode,
|
|
@@ -317,6 +317,15 @@ async function verify(proof, opts) {
|
|
|
317
317
|
catch (_e) { throw new AuthError("auth-dpop/malformed", "header is not valid base64url-JSON"); }
|
|
318
318
|
try { payload = safeJson.parse(_b64urlDecode(parts[1]).toString("utf8")); }
|
|
319
319
|
catch (_e) { throw new AuthError("auth-dpop/malformed", "payload is not valid base64url-JSON"); }
|
|
320
|
+
// safeJson.parse accepts the literal `null` / scalars, so a header segment of
|
|
321
|
+
// base64url("null") would otherwise reach `header.typ` and throw a raw
|
|
322
|
+
// TypeError rather than the typed malformed error. Require JSON objects.
|
|
323
|
+
if (!safeJson.isJsonObject(header)) {
|
|
324
|
+
throw new AuthError("auth-dpop/malformed", "header is not a JSON object");
|
|
325
|
+
}
|
|
326
|
+
if (!safeJson.isJsonObject(payload)) {
|
|
327
|
+
throw new AuthError("auth-dpop/malformed", "payload is not a JSON object");
|
|
328
|
+
}
|
|
320
329
|
|
|
321
330
|
// Header checks
|
|
322
331
|
if (header.typ !== "dpop+jwt") {
|
|
@@ -550,24 +550,37 @@ function lookupAaguid(blob, aaguid) {
|
|
|
550
550
|
return null;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
553
|
+
// Resolve the CURRENT certified level from the status-report history. FIDO MDS3
|
|
554
|
+
// status reports are chronological; the level is whatever the MOST RECENT
|
|
555
|
+
// certification-status report says — a FIDO_CERTIFIED_L{N}[_PLUS], or 0 when the
|
|
556
|
+
// latest such report is NOT_FIDO_CERTIFIED (a decertification). Taking the
|
|
557
|
+
// highest-ever level instead let a step-up / risk policy that requires L{N}
|
|
558
|
+
// accept an authenticator that was later decertified or downgraded (the
|
|
559
|
+
// certifiedLevel is the policy input, so a stale max is an authenticator-
|
|
560
|
+
// assurance bypass). Ordering is by effectiveDate (ISO YYYY-MM-DD, so lexical ==
|
|
561
|
+
// chronological); ties and missing dates fall back to array order (append
|
|
562
|
+
// order, which the spec defines as chronological).
|
|
556
563
|
function _certifiedLevel(statusReports) {
|
|
557
564
|
if (!Array.isArray(statusReports)) return { level: 0, plus: false };
|
|
558
|
-
var
|
|
565
|
+
var latest = null;
|
|
566
|
+
var latestDate = null;
|
|
559
567
|
for (var i = 0; i < statusReports.length; i++) {
|
|
560
568
|
var sr = statusReports[i];
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
|
|
569
|
+
// Only certification-status reports move the level: a level grant, or its
|
|
570
|
+
// explicit revocation (NOT_FIDO_CERTIFIED). Other statuses (UPDATE_AVAILABLE,
|
|
571
|
+
// REVOKED, …) are handled elsewhere and must not be read as a level. The
|
|
572
|
+
// status length is bounded before the regex test below — FIDO status tokens
|
|
573
|
+
// are short enums; bounding input before any .test() is the convention.
|
|
574
|
+
if (!sr || typeof sr.status !== "string" || sr.status.length > 64) continue;
|
|
575
|
+
if (!CERT_LEVEL_RE.test(sr.status) && sr.status !== "NOT_FIDO_CERTIFIED") continue;
|
|
576
|
+
var d = typeof sr.effectiveDate === "string" ? sr.effectiveDate : "";
|
|
577
|
+
if (latest === null || d >= latestDate) {
|
|
578
|
+
latest = sr; latestDate = d;
|
|
568
579
|
}
|
|
569
580
|
}
|
|
570
|
-
return
|
|
581
|
+
if (!latest || latest.status === "NOT_FIDO_CERTIFIED") return { level: 0, plus: false };
|
|
582
|
+
var m = CERT_LEVEL_RE.exec(latest.status);
|
|
583
|
+
return { level: parseInt(m[1], 10), plus: !!m[2] };
|
|
571
584
|
}
|
|
572
585
|
|
|
573
586
|
/**
|
|
@@ -193,6 +193,16 @@ function decode(token) {
|
|
|
193
193
|
catch (_e) { throw new AuthError("auth-jwt/malformed", "header is not valid base64url-JSON"); }
|
|
194
194
|
try { payload = safeJson.parse(_b64urlDecode(parts[1])); }
|
|
195
195
|
catch (_e) { throw new AuthError("auth-jwt/malformed", "payload is not valid base64url-JSON"); }
|
|
196
|
+
// The JOSE header and the claims set MUST each be a JSON object. safeJson.parse
|
|
197
|
+
// accepts the literal `null` (a valid JSON document) and scalars/arrays, so a
|
|
198
|
+
// header segment of base64url("null") would otherwise survive to a `.crit` /
|
|
199
|
+
// `.kid` dereference and throw a raw TypeError instead of a typed AuthError.
|
|
200
|
+
if (!safeJson.isJsonObject(header)) {
|
|
201
|
+
throw new AuthError("auth-jwt/malformed", "header is not a JSON object");
|
|
202
|
+
}
|
|
203
|
+
if (!safeJson.isJsonObject(payload)) {
|
|
204
|
+
throw new AuthError("auth-jwt/malformed", "payload is not a JSON object");
|
|
205
|
+
}
|
|
196
206
|
var signature;
|
|
197
207
|
try { signature = _b64urlDecode(parts[2]); }
|
|
198
208
|
catch (_e) { throw new AuthError("auth-jwt/malformed", "signature is not valid base64url"); }
|
|
@@ -351,10 +361,16 @@ async function verify(token, opts) {
|
|
|
351
361
|
"token not yet valid: nbf=" + p.nbf + " (now=" + nowSec + ", tolerance=" + tol + "s)");
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
// String-claim assertions
|
|
355
|
-
|
|
364
|
+
// String-claim assertions. `iss` is StringOrURI (RFC 7519 §4.1.1) — a single
|
|
365
|
+
// value, not a list: reject a non-string iss before matching. Routing it
|
|
366
|
+
// through the aud-style any-of _matchClaim let an attacker-influenced
|
|
367
|
+
// multi-issuer array (iss:["evil","trusted"]) satisfy a single-issuer
|
|
368
|
+
// expectation (CVE-2025-30144 / fast-jwt iss-array class) — the same defense
|
|
369
|
+
// jwtExternal.verifyExternal and oauth.verifyIdToken already apply.
|
|
370
|
+
if (opts.issuer !== undefined &&
|
|
371
|
+
(typeof p.iss !== "string" || !_matchClaim(p.iss, opts.issuer, "iss"))) {
|
|
356
372
|
throw new AuthError("auth-jwt/iss-mismatch",
|
|
357
|
-
"iss=
|
|
373
|
+
"iss=" + JSON.stringify(p.iss) + " does not match expected " + JSON.stringify(opts.issuer));
|
|
358
374
|
}
|
|
359
375
|
if (opts.audience !== undefined && !_matchClaim(p.aud, opts.audience, "aud")) {
|
|
360
376
|
throw new AuthError("auth-jwt/aud-mismatch",
|
|
@@ -1276,9 +1276,15 @@ function create(opts) {
|
|
|
1276
1276
|
// browser session could be replayed without detection. Throw
|
|
1277
1277
|
// loudly so the operator sees the bug at config time, not at
|
|
1278
1278
|
// first-replay-attempt time.
|
|
1279
|
-
|
|
1279
|
+
// Require a NON-EMPTY STRING nonce, not merely a defined one: a falsy
|
|
1280
|
+
// nonce (null / "" — e.g. a session field that was never set) would slip a
|
|
1281
|
+
// strict `=== undefined` guard, and the downstream verifier only checks the
|
|
1282
|
+
// nonce when vopts.nonce is truthy, so the ID-token nonce check would be
|
|
1283
|
+
// silently skipped and a token captured from another session replayed.
|
|
1284
|
+
if (isOidc && eopts.skipNonceCheck !== true &&
|
|
1285
|
+
(typeof eopts.nonce !== "string" || eopts.nonce.length === 0)) {
|
|
1280
1286
|
throw new OAuthError("auth-oauth/no-nonce",
|
|
1281
|
-
"exchangeCode: nonce is required on OIDC flows. Pass the " +
|
|
1287
|
+
"exchangeCode: a non-empty nonce is required on OIDC flows. Pass the " +
|
|
1282
1288
|
"value returned from authorizationUrl() through to exchangeCode " +
|
|
1283
1289
|
"({ code, state, verifier, nonce }). Operators with a deliberate " +
|
|
1284
1290
|
"no-nonce flow must pass `skipNonceCheck: true` (audited reason).");
|
|
@@ -695,7 +695,7 @@ function create(opts) {
|
|
|
695
695
|
if (nbHok && isFinite(Date.parse(nbHok) / 1000) && // ms→s
|
|
696
696
|
Date.parse(nbHok) / 1000 > nowSec + clockSkewSec) continue; // ms→s
|
|
697
697
|
var recipHok = _attr(scdHok, "Recipient");
|
|
698
|
-
if (recipHok
|
|
698
|
+
if (!recipHok || recipHok !== opts.assertionConsumerServiceUrl) continue; // §3.1→§4.1.4.2 — Recipient is mandatory; absent fails the endpoint binding
|
|
699
699
|
hokOk = true;
|
|
700
700
|
break;
|
|
701
701
|
}
|
|
@@ -711,8 +711,11 @@ function create(opts) {
|
|
|
711
711
|
var nb = Date.parse(notBefore) / 1000; // ms→s
|
|
712
712
|
if (isFinite(nb) && nb > nowSec + clockSkewSec) continue;
|
|
713
713
|
}
|
|
714
|
+
// §4.1.4.2 — a Bearer SubjectConfirmationData delivered to an ACS MUST
|
|
715
|
+
// carry a Recipient equal to this SP's ACS URL. Absent Recipient fails
|
|
716
|
+
// the endpoint binding (treated as a mismatch), not silently skipped.
|
|
714
717
|
var recipient = _attr(scd, "Recipient");
|
|
715
|
-
if (recipient
|
|
718
|
+
if (!recipient || recipient !== opts.assertionConsumerServiceUrl) {
|
|
716
719
|
continue;
|
|
717
720
|
}
|
|
718
721
|
var inResponseTo = _attr(scd, "InResponseTo");
|
|
@@ -774,17 +777,24 @@ function create(opts) {
|
|
|
774
777
|
// SP). Fail closed when an audience is configured; opt out only via
|
|
775
778
|
// vopts.requireAudienceRestriction === false.
|
|
776
779
|
if (audience && vopts.requireAudienceRestriction !== false) {
|
|
777
|
-
var
|
|
778
|
-
|
|
780
|
+
var ars = conditions
|
|
781
|
+
? _findAllChildren(conditions, "AudienceRestriction", SAML_NS.assertion) : [];
|
|
782
|
+
if (ars.length === 0) {
|
|
779
783
|
throw new AuthError("auth-saml/no-audience-restriction",
|
|
780
784
|
"verifyResponse: assertion has no AudienceRestriction binding it to \"" +
|
|
781
785
|
audience + "\" (audience-confusion defense; set requireAudienceRestriction:false to opt out)");
|
|
782
786
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
787
|
+
// SAML core §2.5.1.4: multiple <AudienceRestriction> elements are
|
|
788
|
+
// AND-combined — the SP must be a member of the Audience set of EVERY
|
|
789
|
+
// one. Checking only the first let an IdP that narrowed the assertion to
|
|
790
|
+
// a DIFFERENT audience in a later restriction be accepted here.
|
|
791
|
+
for (var ari = 0; ari < ars.length; ari += 1) {
|
|
792
|
+
var audiences = _findAllChildren(ars[ari], "Audience", SAML_NS.assertion).map(_textContent);
|
|
793
|
+
if (audiences.indexOf(audience) === -1) {
|
|
794
|
+
throw new AuthError("auth-saml/wrong-audience",
|
|
795
|
+
"Audience \"" + audience + "\" not in AudienceRestriction #" + (ari + 1) +
|
|
796
|
+
" of " + ars.length + " (got " + JSON.stringify(audiences) + ")");
|
|
797
|
+
}
|
|
788
798
|
}
|
|
789
799
|
}
|
|
790
800
|
|
|
@@ -1747,9 +1747,27 @@ function assertColumnResidency(table, row, args) {
|
|
|
1747
1747
|
var entry = columnResidency[table];
|
|
1748
1748
|
if (!entry || !row || !args) return null;
|
|
1749
1749
|
var backendTag = args.backendTag || "unrestricted";
|
|
1750
|
+
// SQL unquoted identifiers are case-insensitive; a raw-SQL-parsed row keeps
|
|
1751
|
+
// the column token's case, so resolve each mapped column case-insensitively
|
|
1752
|
+
// (a case-sensitive `row[col]` let a differently-cased column skip the gate —
|
|
1753
|
+
// CWE-178). Lazily index the row's keys by lowercase; exact match wins first.
|
|
1754
|
+
var rowLcIndex = null;
|
|
1755
|
+
function _rowVal(c) {
|
|
1756
|
+
if (Object.prototype.hasOwnProperty.call(row, c)) return row[c];
|
|
1757
|
+
if (rowLcIndex === null) {
|
|
1758
|
+
rowLcIndex = {};
|
|
1759
|
+
var ks = Object.keys(row);
|
|
1760
|
+
for (var i = 0; i < ks.length; i++) {
|
|
1761
|
+
var lk = ks[i].toLowerCase();
|
|
1762
|
+
if (!Object.prototype.hasOwnProperty.call(rowLcIndex, lk)) rowLcIndex[lk] = row[ks[i]];
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return rowLcIndex[String(c).toLowerCase()];
|
|
1766
|
+
}
|
|
1750
1767
|
for (var col in entry) {
|
|
1751
1768
|
var want = entry[col];
|
|
1752
|
-
|
|
1769
|
+
var cellVal = _rowVal(col);
|
|
1770
|
+
if (cellVal === undefined || cellVal === null) continue;
|
|
1753
1771
|
if (want === "global" || want === "unrestricted") continue;
|
|
1754
1772
|
if (backendTag === "unrestricted") continue;
|
|
1755
1773
|
if (backendTag !== want) {
|
|
@@ -210,6 +210,15 @@ function build(directives, opts) {
|
|
|
210
210
|
throw new CspError("csp/header-injection",
|
|
211
211
|
"csp.build: source '" + src + "' contains CR/LF/NUL");
|
|
212
212
|
}
|
|
213
|
+
// A CSP source-expression is a single non-whitespace token. The emitter
|
|
214
|
+
// space-joins sources within a directive and ';'-joins directives, so a
|
|
215
|
+
// source containing ';' or ASCII whitespace would inject a brand-new live
|
|
216
|
+
// directive (e.g. "https://x; script-src https://evil") — refuse both.
|
|
217
|
+
if (/[\s;]/.test(src)) {
|
|
218
|
+
throw new CspError("csp/bad-source",
|
|
219
|
+
"csp.build: source '" + src + "' contains whitespace or ';' — a CSP source " +
|
|
220
|
+
"must be a single token (directive-injection defense)");
|
|
221
|
+
}
|
|
213
222
|
if (!acknowledgeUnsafe && SCRIPT_DIRECTIVES.indexOf(name) !== -1 &&
|
|
214
223
|
UNSAFE_KEYWORDS.indexOf(src) !== -1) {
|
|
215
224
|
throw new CspError("csp/unsafe-keyword",
|
|
@@ -89,6 +89,19 @@ function _postureState() {
|
|
|
89
89
|
//
|
|
90
90
|
// Unregulated postures audit (drop-silent) and pass; tables with no
|
|
91
91
|
// declaration are untouched.
|
|
92
|
+
// Resolve a column in a raw-SQL-parsed row case-insensitively (SQL unquoted
|
|
93
|
+
// identifiers fold case; the parser preserves the token). Exact match wins
|
|
94
|
+
// first so a structured-builder row (keys already canonical) is unaffected.
|
|
95
|
+
function _ciColumn(row, col) {
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(row, col)) return { present: true, value: row[col] };
|
|
97
|
+
var lc = String(col).toLowerCase();
|
|
98
|
+
var keys = Object.keys(row);
|
|
99
|
+
for (var i = 0; i < keys.length; i++) {
|
|
100
|
+
if (keys[i].toLowerCase() === lc) return { present: true, value: row[keys[i]] };
|
|
101
|
+
}
|
|
102
|
+
return { present: false, value: undefined };
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
function _assertLocalResidency(table, plaintextRow, op) {
|
|
93
106
|
var spec = cryptoField.getPerRowResidency(table);
|
|
94
107
|
var colMap = cryptoField.getColumnResidency(table);
|
|
@@ -106,9 +119,16 @@ function _assertLocalResidency(table, plaintextRow, op) {
|
|
|
106
119
|
var regulated = state.regulated;
|
|
107
120
|
|
|
108
121
|
if (spec) {
|
|
109
|
-
|
|
122
|
+
// SQL unquoted identifiers are case-insensitive, and the raw-SQL parser
|
|
123
|
+
// preserves the column token's case — so resolve the residency column
|
|
124
|
+
// case-insensitively. A case-sensitive lookup let `UPDATE t SET REGION=...`
|
|
125
|
+
// miss a `residencyColumn: "region"` declaration, skipping the gate and
|
|
126
|
+
// admitting a cross-border write (CWE-178 / CWE-863). Fail-safe: any
|
|
127
|
+
// spelling that could be the residency column engages the gate.
|
|
128
|
+
var resolved = _ciColumn(plaintextRow, spec.residencyColumn);
|
|
129
|
+
var tag = resolved.value;
|
|
110
130
|
var tagPresent = tag !== undefined && tag !== null;
|
|
111
|
-
var colInChangeSet =
|
|
131
|
+
var colInChangeSet = resolved.present;
|
|
112
132
|
if (op === "insert" && !tagPresent) {
|
|
113
133
|
throw new DbQueryError("db-query/row-residency-tag-missing",
|
|
114
134
|
op + ": table '" + table + "' declares per-row residency on column '" +
|
|
@@ -368,6 +388,17 @@ class Query {
|
|
|
368
388
|
return this.where(field, "IN", values);
|
|
369
389
|
}
|
|
370
390
|
|
|
391
|
+
// whereNull / whereNotNull — explicit NULL predicates (IS NULL / IS NOT
|
|
392
|
+
// NULL). `where({ field: null })` / `where(field, "=", null)` is refused
|
|
393
|
+
// because `col = NULL` is UNKNOWN in SQL (never true); these are the
|
|
394
|
+
// intended way to test a column for NULL.
|
|
395
|
+
whereNull(field) {
|
|
396
|
+
return this.where(field, "IS", null);
|
|
397
|
+
}
|
|
398
|
+
whereNotNull(field) {
|
|
399
|
+
return this.where(field, "IS NOT", null);
|
|
400
|
+
}
|
|
401
|
+
|
|
371
402
|
// Resolve a (field, op, value) predicate through the framework gates
|
|
372
403
|
// (JSONB value guard, sealed-field → derived-hash rewrite, column
|
|
373
404
|
// membership) and return the post-rewrite { field, op, value } that
|
|
@@ -2173,15 +2173,38 @@ function _extractFromHeaders(headerBlock) {
|
|
|
2173
2173
|
// one of several authors (the §6.6.1 forbidden move), so the
|
|
2174
2174
|
// field is treated as unparsable instead.
|
|
2175
2175
|
address = value.trim();
|
|
2176
|
-
|
|
2176
|
+
// An RFC 5322 addr-spec cannot contain whitespace, commas, or the
|
|
2177
|
+
// group / route markers ; : < > — their presence means a list, a
|
|
2178
|
+
// display-name, or RFC 5322 group syntax (`display-name: mailbox ;`),
|
|
2179
|
+
// not a single bare addr-spec.
|
|
2180
|
+
if (/[\s,;:<>]/.test(address)) address = null;
|
|
2177
2181
|
}
|
|
2178
2182
|
var at = address ? address.lastIndexOf("@") : -1;
|
|
2179
2183
|
var domain = (at > 0 && address && at < address.length - 1)
|
|
2180
2184
|
? address.slice(at + 1).toLowerCase()
|
|
2181
2185
|
: null;
|
|
2186
|
+
// The domain drives the DMARC policy lookup + alignment check. A value that
|
|
2187
|
+
// is not a syntactically valid DNS hostname (a trailing ';', group-syntax
|
|
2188
|
+
// soup, or other bytes that slipped the filters) would silently miss the
|
|
2189
|
+
// policy record and defeat DMARC (CWE-290). Require a real hostname;
|
|
2190
|
+
// otherwise treat From as unparsable — the caller fails closed (permerror →
|
|
2191
|
+
// reject).
|
|
2192
|
+
if (domain && !_isValidFromHostname(domain)) domain = null;
|
|
2182
2193
|
return { count: count, address: address || null, domain: domain };
|
|
2183
2194
|
}
|
|
2184
2195
|
|
|
2196
|
+
// Strict, bounded DNS-hostname check (ASCII / A-label form, as DMARC lookups
|
|
2197
|
+
// use). Anchored with a length lookahead and tempered labels — linear time, no
|
|
2198
|
+
// catastrophic backtracking. Requires at least two labels (a dot).
|
|
2199
|
+
var _FROM_HOSTNAME_RE =
|
|
2200
|
+
/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
2201
|
+
function _isValidFromHostname(host) {
|
|
2202
|
+
// Bound the length before the regex test (max DNS name is 253 octets) — the
|
|
2203
|
+
// pattern is already anchored + tempered, but bounding untrusted input before
|
|
2204
|
+
// any .test() is the framework convention.
|
|
2205
|
+
return typeof host === "string" && host.length <= 253 && _FROM_HOSTNAME_RE.test(host);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2185
2208
|
async function inboundVerify(opts) {
|
|
2186
2209
|
validateOpts.requireObject(opts, "inbound.verify", MailAuthError, "mail-auth/inbound-bad-input");
|
|
2187
2210
|
validateOpts(opts, ["ip", "helo", "mailFrom", "message", "dnsLookup", "domainExists",
|
|
@@ -167,13 +167,24 @@ function _parseHeaders(rawHeaders) {
|
|
|
167
167
|
|
|
168
168
|
// ---- Hashing + signing ----
|
|
169
169
|
|
|
170
|
-
function _bodyHashB64(body, algorithm, canonBody) {
|
|
170
|
+
function _bodyHashB64(body, algorithm, canonBody, lcap) {
|
|
171
171
|
var canonicalized = canonBody === "simple"
|
|
172
172
|
? _canonBodySimple(body)
|
|
173
173
|
: _canonBodyRelaxed(body);
|
|
174
174
|
var hashName = "sha256"; // both rsa-sha256 and ed25519-sha256 hash with sha256
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
var hash = nodeCrypto.createHash(hashName);
|
|
176
|
+
// RFC 6376 §3.4.5 — the l= body-length count is octets of the body AFTER
|
|
177
|
+
// canonicalization. Canonicalize first, then truncate the canonicalized
|
|
178
|
+
// octet stream to lcap (slicing the raw body before canonicalizing diverges
|
|
179
|
+
// whenever relaxed canon changes the byte count within the first lcap
|
|
180
|
+
// octets — WSP-run collapse, trailing-WSP strip, CRLF normalization).
|
|
181
|
+
if (typeof lcap === "number" && isFinite(lcap) && lcap >= 0) {
|
|
182
|
+
var buf = Buffer.from(canonicalized, "utf8");
|
|
183
|
+
hash.update(lcap < buf.length ? buf.subarray(0, lcap) : buf);
|
|
184
|
+
} else {
|
|
185
|
+
hash.update(canonicalized);
|
|
186
|
+
}
|
|
187
|
+
return hash.digest("base64");
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
function _signString(strToSign, privateKey, algorithm) {
|
|
@@ -674,15 +685,17 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
|
|
|
674
685
|
|
|
675
686
|
var split = _splitHeadersBody(rfc822);
|
|
676
687
|
var body = split.body;
|
|
688
|
+
var lcap;
|
|
677
689
|
if (sigTags.l !== undefined) {
|
|
678
690
|
// The framework refuses l= at SIGN-time per the M3AAWG / Gmail /
|
|
679
691
|
// Microsoft 365 guidance (v0.7.18). On VERIFY, an `l=` tag on an
|
|
680
692
|
// inbound signature signals append-after-signature exposure —
|
|
681
693
|
// operators decide acceptance. Honor the cap for the body hash so
|
|
682
694
|
// the signature still validates against legitimate senders that
|
|
683
|
-
// use l=, but flag in the result.
|
|
684
|
-
|
|
685
|
-
|
|
695
|
+
// use l=, but flag in the result. The cap is octets of the
|
|
696
|
+
// CANONICALIZED body (RFC 6376 §3.4.5), applied inside _bodyHashB64.
|
|
697
|
+
var parsedL = parseInt(sigTags.l, 10);
|
|
698
|
+
if (isFinite(parsedL) && parsedL >= 0) lcap = parsedL;
|
|
686
699
|
}
|
|
687
700
|
|
|
688
701
|
// 1. Body-hash check.
|
|
@@ -690,7 +703,7 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
|
|
|
690
703
|
if (typeof expectedBh !== "string") {
|
|
691
704
|
return { result: "permerror", errors: ["DKIM-Signature missing bh="] };
|
|
692
705
|
}
|
|
693
|
-
var actualBh = _bodyHashB64(body, algorithm, canonBody);
|
|
706
|
+
var actualBh = _bodyHashB64(body, algorithm, canonBody, lcap);
|
|
694
707
|
if (actualBh !== expectedBh) {
|
|
695
708
|
return { result: "fail", errors: ["body hash mismatch"] };
|
|
696
709
|
}
|
|
@@ -287,6 +287,28 @@ function composePipeline(entries, opts) {
|
|
|
287
287
|
catch (finalErr) { return reject(finalErr); }
|
|
288
288
|
resolve();
|
|
289
289
|
}
|
|
290
|
+
// A middleware (or error handler) that ENDS THE RESPONSE without calling
|
|
291
|
+
// next has halted the chain: it handled the request itself. Settle the
|
|
292
|
+
// outer promise so the awaiting router is released — a never-settled
|
|
293
|
+
// promise pins its req/res closure forever — but do NOT call finalNext:
|
|
294
|
+
// the router's next-flag stays false, so it won't run the route handler
|
|
295
|
+
// on top of an already-sent response.
|
|
296
|
+
function _resolveOnce() {
|
|
297
|
+
if (finished) return;
|
|
298
|
+
finished = true;
|
|
299
|
+
resolve();
|
|
300
|
+
}
|
|
301
|
+
// Settle when the response actually finishes. This is response-driven,
|
|
302
|
+
// not return-driven: a callback-style middleware that calls next() LATER
|
|
303
|
+
// (from a timer, stream, or legacy callback) returns before next() runs,
|
|
304
|
+
// so we must NOT treat a bare return as a halt — only an ended response.
|
|
305
|
+
// The synchronous _responseEnded() check below covers a middleware that
|
|
306
|
+
// ended the response inline (and a mock res without an event emitter);
|
|
307
|
+
// this listener covers one that ends it from a deferred callback.
|
|
308
|
+
if (res && typeof res.once === "function") {
|
|
309
|
+
res.once("finish", _resolveOnce);
|
|
310
|
+
res.once("close", _resolveOnce);
|
|
311
|
+
}
|
|
290
312
|
async function dispatch(err) {
|
|
291
313
|
if (finished) return;
|
|
292
314
|
if (idx >= resolved.length) return _finishOnce(err);
|
|
@@ -313,14 +335,19 @@ function composePipeline(entries, opts) {
|
|
|
313
335
|
}
|
|
314
336
|
try {
|
|
315
337
|
if (err) {
|
|
316
|
-
// Error handler: (err, req, res, next)
|
|
338
|
+
// Error handler: (err, req, res, next). Express convention — a
|
|
339
|
+
// 4-arg handler that returns without calling next has HANDLED the
|
|
340
|
+
// error, so the chain ends cleanly; settle (without finalNext).
|
|
317
341
|
await entry.mw(err, req, res, _next);
|
|
318
|
-
if (!advanced)
|
|
342
|
+
if (!advanced) _resolveOnce();
|
|
319
343
|
} else {
|
|
320
|
-
// Regular middleware: (req, res, next)
|
|
344
|
+
// Regular middleware: (req, res, next). Settle only if it ENDED the
|
|
345
|
+
// response (a halt). A bare return without next is NOT treated as a
|
|
346
|
+
// halt — it may be a callback-style middleware that calls next()
|
|
347
|
+
// later (timer/stream); the finish/close listener covers a deferred
|
|
348
|
+
// response end, and a deferred next() continues the chain.
|
|
321
349
|
await entry.mw(req, res, _next);
|
|
322
|
-
|
|
323
|
-
// The middleware presumably wrote the response itself.
|
|
350
|
+
if (!advanced && _responseEnded(res)) _resolveOnce();
|
|
324
351
|
}
|
|
325
352
|
} catch (syncErr) {
|
|
326
353
|
// Synchronous throw OR rejected promise — route through
|
|
@@ -334,6 +361,13 @@ function composePipeline(entries, opts) {
|
|
|
334
361
|
};
|
|
335
362
|
}
|
|
336
363
|
|
|
364
|
+
// True once the response has been committed/ended — the reliable "this
|
|
365
|
+
// middleware handled the request" signal (vs. the function merely returning,
|
|
366
|
+
// which a callback-style middleware does before its deferred next()).
|
|
367
|
+
function _responseEnded(res) {
|
|
368
|
+
return !!(res && (res.writableEnded || res.finished || res.headersSent));
|
|
369
|
+
}
|
|
370
|
+
|
|
337
371
|
composePipeline.CANONICAL_POSITIONS = CANONICAL_POSITIONS;
|
|
338
372
|
composePipeline.ComposePipelineError = ComposePipelineError;
|
|
339
373
|
|
|
@@ -108,14 +108,17 @@ function _requireRecordedAt(value, label) {
|
|
|
108
108
|
* and determine the lawful mechanism the transfer requires. PIPL Art. 38(1)
|
|
109
109
|
* permits three bases for moving personal information out of the PRC — the
|
|
110
110
|
* CAC standard contract (SCC), a CAC security assessment (Art. 40), or
|
|
111
|
-
* certification by a CAC-accredited body
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* operator (CIIO),
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* individuals
|
|
111
|
+
* certification by a CAC-accredited body. Under the CAC Provisions on
|
|
112
|
+
* Promoting and Regulating Cross-Border Data Flows (effective 2024, which
|
|
113
|
+
* relaxed the 2022 thresholds) the security assessment is MANDATORY (the
|
|
114
|
+
* operator may NOT self-select the standard contract or certification) when
|
|
115
|
+
* the exporter is a critical-information-infrastructure operator (CIIO),
|
|
116
|
+
* exports "important data", or — counting cumulatively since 1 January of
|
|
117
|
+
* the current year — transfers the personal information of more than
|
|
118
|
+
* 1,000,000 individuals (non-sensitive) or the sensitive personal
|
|
119
|
+
* information of more than 10,000 individuals. The 100,000–1,000,000
|
|
120
|
+
* non-sensitive band is the standard-contract / certification tier, NOT a
|
|
121
|
+
* security-assessment trigger.
|
|
119
122
|
*
|
|
120
123
|
* The builder validates the operator-supplied facts, computes
|
|
121
124
|
* `securityAssessmentRequired` against those thresholds, resolves the
|