@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
|
@@ -384,6 +384,134 @@ function trustedClientIp(opts) {
|
|
|
384
384
|
};
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// IP-prefix masking constants — named so the bit-arithmetic stays readable.
|
|
388
|
+
// /24 IPv4 is the original IP-geolocation bucket and matches the legacy
|
|
389
|
+
// carrier-NAT pool stride; /64 IPv6 is the customer LAN every RIR allocates
|
|
390
|
+
// (RFC 4291 §2.5.4), so tightening below it punishes IPv6 privacy-extension
|
|
391
|
+
// address rotation.
|
|
392
|
+
var IP_BITS_PER_BYTE = 8; // bits per byte; protocol constant, not a byte size
|
|
393
|
+
var IPV4_OCTET_COUNT = 4;
|
|
394
|
+
var IPV4_OCTET_RANGE = 256; // 0..255 inclusive; v4 octet domain
|
|
395
|
+
var IPV4_TOTAL_BITS = 32; // IPv4 address width in bits
|
|
396
|
+
var IPV4_DEFAULT_PREFIX = 24; // /24 carrier-NAT pool stride
|
|
397
|
+
var IPV6_GROUP_COUNT = 8; // 8 16-bit groups in v6
|
|
398
|
+
var IPV6_BYTE_COUNT = 16; // 16 bytes in v6
|
|
399
|
+
var IPV6_DEFAULT_PREFIX = 64; // /64 customer LAN per RFC 4291 §2.5.4
|
|
400
|
+
var IP_BYTE_MASK = 0xff;
|
|
401
|
+
var IP_HEX_RADIX = 16; // base-16 radix
|
|
402
|
+
var V4_MAPPED_V6_PREFIX = "::ffff:";
|
|
403
|
+
|
|
404
|
+
function _maskIpv4(ip, prefix) {
|
|
405
|
+
// ip = "a.b.c.d"; prefix is bits to keep (1..32).
|
|
406
|
+
var parts = String(ip).split(".");
|
|
407
|
+
if (parts.length !== IPV4_OCTET_COUNT) return null;
|
|
408
|
+
var n = 0;
|
|
409
|
+
for (var i = 0; i < IPV4_OCTET_COUNT; i++) {
|
|
410
|
+
var oct = parseInt(parts[i], 10);
|
|
411
|
+
if (!Number.isInteger(oct) || oct < 0 || oct >= IPV4_OCTET_RANGE) return null;
|
|
412
|
+
n = (n * IPV4_OCTET_RANGE) + oct;
|
|
413
|
+
}
|
|
414
|
+
// Apply prefix mask.
|
|
415
|
+
var mask = prefix === 0 ? 0 : (-1 >>> (IPV4_TOTAL_BITS - prefix)) << (IPV4_TOTAL_BITS - prefix);
|
|
416
|
+
// Bitwise on 32-bit unsigned. JS coerces to 32-bit signed, so use
|
|
417
|
+
// unsigned right shift to recover.
|
|
418
|
+
var masked = (n & mask) >>> 0;
|
|
419
|
+
return ((masked >>> IP_BITS_PER_BYTE * 3) & IP_BYTE_MASK) + "." +
|
|
420
|
+
((masked >>> IP_BITS_PER_BYTE * 2) & IP_BYTE_MASK) + "." +
|
|
421
|
+
((masked >>> IP_BITS_PER_BYTE) & IP_BYTE_MASK) + "." +
|
|
422
|
+
(masked & IP_BYTE_MASK) + "/" + prefix;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function _maskIpv6(ip, prefix) {
|
|
426
|
+
// Expand to 8 16-bit groups. Accept :: shorthand. Reject if invalid.
|
|
427
|
+
var raw = String(ip).toLowerCase();
|
|
428
|
+
// Strip an embedded zone id (fe80::1%eth0); not part of the address.
|
|
429
|
+
var pct = raw.indexOf("%");
|
|
430
|
+
if (pct !== -1) raw = raw.substring(0, pct);
|
|
431
|
+
var doubleColonAt = raw.indexOf("::");
|
|
432
|
+
var groups;
|
|
433
|
+
if (doubleColonAt === -1) {
|
|
434
|
+
groups = raw.split(":");
|
|
435
|
+
if (groups.length !== IPV6_GROUP_COUNT) return null;
|
|
436
|
+
} else {
|
|
437
|
+
var left = raw.substring(0, doubleColonAt).split(":");
|
|
438
|
+
var right = raw.substring(doubleColonAt + 2).split(":");
|
|
439
|
+
if (left.length === 1 && left[0] === "") left = [];
|
|
440
|
+
if (right.length === 1 && right[0] === "") right = [];
|
|
441
|
+
var fillCount = IPV6_GROUP_COUNT - left.length - right.length;
|
|
442
|
+
if (fillCount < 0) return null;
|
|
443
|
+
var middle = [];
|
|
444
|
+
for (var fi = 0; fi < fillCount; fi++) middle.push("0");
|
|
445
|
+
groups = left.concat(middle).concat(right);
|
|
446
|
+
}
|
|
447
|
+
// Each group is 1–4 hex chars.
|
|
448
|
+
var bytes = [];
|
|
449
|
+
for (var gi = 0; gi < IPV6_GROUP_COUNT; gi++) {
|
|
450
|
+
var g = groups[gi];
|
|
451
|
+
if (typeof g !== "string" || g.length === 0 || g.length > 4 || /[^0-9a-f]/.test(g)) return null;
|
|
452
|
+
var v = parseInt(g, IP_HEX_RADIX);
|
|
453
|
+
if (!Number.isInteger(v) || v < 0 || v > 0xffff) return null;
|
|
454
|
+
bytes.push((v >> IP_BITS_PER_BYTE) & IP_BYTE_MASK);
|
|
455
|
+
bytes.push(v & IP_BYTE_MASK);
|
|
456
|
+
}
|
|
457
|
+
// Apply prefix in bits.
|
|
458
|
+
var keepBytes = Math.floor(prefix / IP_BITS_PER_BYTE);
|
|
459
|
+
var keepBits = prefix % IP_BITS_PER_BYTE;
|
|
460
|
+
for (var bi = 0; bi < IPV6_BYTE_COUNT; bi++) {
|
|
461
|
+
if (bi < keepBytes) continue;
|
|
462
|
+
if (bi === keepBytes && keepBits > 0) {
|
|
463
|
+
var m = (IP_BYTE_MASK << (IP_BITS_PER_BYTE - keepBits)) & IP_BYTE_MASK;
|
|
464
|
+
bytes[bi] = bytes[bi] & m;
|
|
465
|
+
} else {
|
|
466
|
+
bytes[bi] = 0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Re-emit as colon-hex (no compression — deterministic for hashing).
|
|
470
|
+
var out = [];
|
|
471
|
+
for (var oi = 0; oi < IPV6_BYTE_COUNT; oi += 2) {
|
|
472
|
+
out.push(((bytes[oi] << IP_BITS_PER_BYTE) | bytes[oi + 1]).toString(IP_HEX_RADIX));
|
|
473
|
+
}
|
|
474
|
+
return out.join(":") + "/" + prefix;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @primitive b.requestHelpers.ipPrefix
|
|
479
|
+
* @signature b.requestHelpers.ipPrefix(ip)
|
|
480
|
+
* @since 0.15.15
|
|
481
|
+
* @related b.requestHelpers.clientIp, b.requestHelpers.trustedClientIp
|
|
482
|
+
*
|
|
483
|
+
* Mask a client IP to its subnet bucket: a <code>/24</code> for IPv4 (the
|
|
484
|
+
* carrier-NAT pool stride) and a <code>/64</code> for IPv6 (the customer-LAN
|
|
485
|
+
* prefix RIRs allocate, RFC 4291 §2.5.4). Returns the canonical
|
|
486
|
+
* <code>"network/prefix"</code> string, or <code>""</code> for a non-string /
|
|
487
|
+
* empty / unparseable input. An IPv4-mapped IPv6 address
|
|
488
|
+
* (<code>::ffff:1.2.3.4</code>) folds to its dotted form so it buckets the
|
|
489
|
+
* same regardless of how a proxy reported it.
|
|
490
|
+
*
|
|
491
|
+
* This is the masking the session device-fingerprint's built-in
|
|
492
|
+
* <code>clientIpPrefix</code> field hashes (so roaming carriers that flip the
|
|
493
|
+
* public IP within a subnet don't log a user out). Exposed so an operator who
|
|
494
|
+
* drops to a function-form fingerprint field — for a custom mask width, or to
|
|
495
|
+
* combine the prefix with other signals — reuses this exact algorithm instead
|
|
496
|
+
* of re-deriving the /24 + /64 masking (and silently diverging).
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* b.requestHelpers.ipPrefix("203.0.113.47"); // → "203.0.113.0/24"
|
|
500
|
+
* b.requestHelpers.ipPrefix("2001:db8::1"); // → "2001:db8:0:0/64"
|
|
501
|
+
*/
|
|
502
|
+
function ipPrefix(ip) {
|
|
503
|
+
if (typeof ip !== "string" || ip.length === 0) return "";
|
|
504
|
+
// IPv4-mapped IPv6 (::ffff:1.2.3.4) — strip the wrapper so the v4 mask
|
|
505
|
+
// applies. Same bucket regardless of how the proxy reported it.
|
|
506
|
+
var lower = ip.toLowerCase();
|
|
507
|
+
if (lower.indexOf(V4_MAPPED_V6_PREFIX) === 0 && lower.indexOf(".") !== -1) {
|
|
508
|
+
return _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length), IPV4_DEFAULT_PREFIX) || "";
|
|
509
|
+
}
|
|
510
|
+
if (ip.indexOf(":") !== -1) return _maskIpv6(ip, IPV6_DEFAULT_PREFIX) || "";
|
|
511
|
+
if (ip.indexOf(".") !== -1) return _maskIpv4(ip, IPV4_DEFAULT_PREFIX) || "";
|
|
512
|
+
return "";
|
|
513
|
+
}
|
|
514
|
+
|
|
387
515
|
/**
|
|
388
516
|
* @primitive b.requestHelpers.trustedProtocol
|
|
389
517
|
* @signature b.requestHelpers.trustedProtocol(opts?)
|
|
@@ -787,8 +915,6 @@ function captureResponseStatus(res, onEnd) {
|
|
|
787
915
|
return origEnd;
|
|
788
916
|
}
|
|
789
917
|
|
|
790
|
-
var Q_VALUE_RE = /(?:^|;|\s)q\s*=\s*([0-9]*\.?[0-9]+)/i;
|
|
791
|
-
|
|
792
918
|
/**
|
|
793
919
|
* @primitive b.requestHelpers.parseQualityList
|
|
794
920
|
* @signature b.requestHelpers.parseQualityList(headerValue, opts?)
|
|
@@ -831,24 +957,30 @@ function parseQualityList(headerValue, opts) {
|
|
|
831
957
|
if (typeof headerValue !== "string" || headerValue.length === 0) return [];
|
|
832
958
|
opts = opts || {};
|
|
833
959
|
var caseSensitive = opts.caseSensitive === true;
|
|
834
|
-
|
|
960
|
+
// Quote-aware split: a parameter value may be a quoted-string containing ','
|
|
961
|
+
// or ';' or 'q=' (RFC 7231 §5.3.1 / RFC 9110), which must not split an element
|
|
962
|
+
// or be mis-read as the q-value. splitUnquoted respects double-quoted runs.
|
|
963
|
+
var parts = structuredFields.splitUnquoted(headerValue, ",");
|
|
835
964
|
var out = [];
|
|
836
965
|
for (var i = 0; i < parts.length; i++) {
|
|
837
966
|
var p = parts[i].trim();
|
|
838
967
|
if (p.length === 0) continue;
|
|
839
|
-
var
|
|
840
|
-
var
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
var
|
|
968
|
+
var segs = structuredFields.splitUnquoted(p, ";");
|
|
969
|
+
var head = segs[0].trim();
|
|
970
|
+
var value = caseSensitive ? head : head.toLowerCase();
|
|
971
|
+
var q = 1;
|
|
972
|
+
// The q parameter is the named token `q` separating media-type params from
|
|
973
|
+
// accept-ext; the FIRST `q` parameter is the quality. Only a parameter
|
|
974
|
+
// literally named `q` counts — never a `q=`-shaped substring of another
|
|
975
|
+
// parameter's name or quoted value.
|
|
976
|
+
for (var s = 1; s < segs.length; s++) {
|
|
977
|
+
var kv = structuredFields.parseKeyValuePiece(segs[s], "=", true);
|
|
978
|
+
if (kv.key !== "q") continue;
|
|
979
|
+
var qm = String(kv.value).trim().match(/^([0-9]*\.?[0-9]+)/);
|
|
849
980
|
q = qm ? parseFloat(qm[1]) : 1;
|
|
850
981
|
if (isNaN(q) || q < 0) q = 0;
|
|
851
982
|
if (q > 1) q = 1;
|
|
983
|
+
break;
|
|
852
984
|
}
|
|
853
985
|
out.push({ value: value, q: q });
|
|
854
986
|
}
|
|
@@ -1067,6 +1199,7 @@ module.exports = {
|
|
|
1067
1199
|
// proxy-trust primitives (default refuses forwarded headers)
|
|
1068
1200
|
clientIp: clientIp,
|
|
1069
1201
|
trustedClientIp: trustedClientIp,
|
|
1202
|
+
ipPrefix: ipPrefix,
|
|
1070
1203
|
requestProtocol: requestProtocol,
|
|
1071
1204
|
trustedProtocol: trustedProtocol,
|
|
1072
1205
|
appendVary: appendVary,
|
|
@@ -969,9 +969,35 @@ function _capInt(value, defaultValue, ceiling) {
|
|
|
969
969
|
* });
|
|
970
970
|
*/
|
|
971
971
|
|
|
972
|
+
/**
|
|
973
|
+
* @primitive b.safeJson.isJsonObject
|
|
974
|
+
* @signature b.safeJson.isJsonObject(value)
|
|
975
|
+
* @since 0.15.14
|
|
976
|
+
* @status stable
|
|
977
|
+
* @related b.safeJson.parse
|
|
978
|
+
*
|
|
979
|
+
* True iff <code>value</code> is a plain JSON object — not <code>null</code>,
|
|
980
|
+
* not an array, not a scalar. <code>safeJson.parse</code> accepts the literal
|
|
981
|
+
* <code>null</code> and scalars / arrays (all valid JSON documents), so a
|
|
982
|
+
* parsed JWS header, claims set, or document must be re-checked before its
|
|
983
|
+
* fields are dereferenced. This is that check, shared so the
|
|
984
|
+
* <code>!x || typeof x !== "object" || Array.isArray(x)</code> idiom isn't
|
|
985
|
+
* re-rolled (and silently varied) at every call site.
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* var b = require("blamejs");
|
|
989
|
+
* b.safeJson.isJsonObject(b.safeJson.parse('{"a":1}')); // → true
|
|
990
|
+
* b.safeJson.isJsonObject(b.safeJson.parse("null")); // → false
|
|
991
|
+
* b.safeJson.isJsonObject(b.safeJson.parse("[1,2]")); // → false
|
|
992
|
+
*/
|
|
993
|
+
function isJsonObject(value) {
|
|
994
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
995
|
+
}
|
|
996
|
+
|
|
972
997
|
module.exports = {
|
|
973
998
|
parse: parse,
|
|
974
999
|
parseOrDefault: parseOrDefault,
|
|
1000
|
+
isJsonObject: isJsonObject,
|
|
975
1001
|
stringify: stringify,
|
|
976
1002
|
stringifyForScript: stringifyForScript,
|
|
977
1003
|
canonical: canonical,
|
|
@@ -256,130 +256,48 @@ function _sealForInsert(row) {
|
|
|
256
256
|
var DEFAULT_FINGERPRINT_FIELDS = ["clientIp", "userAgent", "acceptLanguage"];
|
|
257
257
|
|
|
258
258
|
// Subnet binding: roaming carriers (T-Mobile / Verizon / etc.) flip the
|
|
259
|
-
// public client IP every few requests as the device hops cells, so a
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
function _maskIpv4(ip, prefix) {
|
|
287
|
-
// ip = "a.b.c.d"; prefix is bits to keep (1..32).
|
|
288
|
-
var parts = String(ip).split(".");
|
|
289
|
-
if (parts.length !== IPV4_OCTET_COUNT) return null;
|
|
290
|
-
var n = 0;
|
|
291
|
-
for (var i = 0; i < IPV4_OCTET_COUNT; i++) {
|
|
292
|
-
var oct = parseInt(parts[i], 10);
|
|
293
|
-
if (!Number.isInteger(oct) || oct < 0 || oct >= IPV4_OCTET_RANGE) return null;
|
|
294
|
-
n = (n * IPV4_OCTET_RANGE) + oct;
|
|
295
|
-
}
|
|
296
|
-
// Apply prefix mask.
|
|
297
|
-
var mask = prefix === 0 ? 0 : (-1 >>> (IPV4_TOTAL_BITS - prefix)) << (IPV4_TOTAL_BITS - prefix);
|
|
298
|
-
// Bitwise on 32-bit unsigned. JS coerces to 32-bit signed, so use
|
|
299
|
-
// unsigned right shift to recover.
|
|
300
|
-
var masked = (n & mask) >>> 0;
|
|
301
|
-
return ((masked >>> IP_BITS_PER_BYTE * 3) & BYTE_MASK) + "." +
|
|
302
|
-
((masked >>> IP_BITS_PER_BYTE * 2) & BYTE_MASK) + "." +
|
|
303
|
-
((masked >>> IP_BITS_PER_BYTE) & BYTE_MASK) + "." +
|
|
304
|
-
(masked & BYTE_MASK) + "/" + prefix;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function _maskIpv6(ip, prefix) {
|
|
308
|
-
// Expand to 8 16-bit groups. Accept :: shorthand. Reject if invalid.
|
|
309
|
-
var raw = String(ip).toLowerCase();
|
|
310
|
-
// Strip an embedded zone id (fe80::1%eth0); not part of the address.
|
|
311
|
-
var pct = raw.indexOf("%");
|
|
312
|
-
if (pct !== -1) raw = raw.substring(0, pct);
|
|
313
|
-
var doubleColonAt = raw.indexOf("::");
|
|
314
|
-
var groups;
|
|
315
|
-
if (doubleColonAt === -1) {
|
|
316
|
-
groups = raw.split(":");
|
|
317
|
-
if (groups.length !== IPV6_GROUP_COUNT) return null;
|
|
318
|
-
} else {
|
|
319
|
-
var left = raw.substring(0, doubleColonAt).split(":");
|
|
320
|
-
var right = raw.substring(doubleColonAt + 2).split(":");
|
|
321
|
-
if (left.length === 1 && left[0] === "") left = [];
|
|
322
|
-
if (right.length === 1 && right[0] === "") right = [];
|
|
323
|
-
var fillCount = IPV6_GROUP_COUNT - left.length - right.length;
|
|
324
|
-
if (fillCount < 0) return null;
|
|
325
|
-
var middle = [];
|
|
326
|
-
for (var fi = 0; fi < fillCount; fi++) middle.push("0");
|
|
327
|
-
groups = left.concat(middle).concat(right);
|
|
328
|
-
}
|
|
329
|
-
// Each group is 1–4 hex chars.
|
|
330
|
-
var bytes = [];
|
|
331
|
-
for (var gi = 0; gi < IPV6_GROUP_COUNT; gi++) {
|
|
332
|
-
var g = groups[gi];
|
|
333
|
-
if (typeof g !== "string" || g.length === 0 || g.length > 4 || /[^0-9a-f]/.test(g)) return null;
|
|
334
|
-
var v = parseInt(g, HEX_RADIX);
|
|
335
|
-
if (!Number.isInteger(v) || v < 0 || v > 0xffff) return null;
|
|
336
|
-
bytes.push((v >> IP_BITS_PER_BYTE) & BYTE_MASK);
|
|
337
|
-
bytes.push(v & BYTE_MASK);
|
|
338
|
-
}
|
|
339
|
-
// Apply prefix in bits.
|
|
340
|
-
var keepBytes = Math.floor(prefix / IP_BITS_PER_BYTE);
|
|
341
|
-
var keepBits = prefix % IP_BITS_PER_BYTE;
|
|
342
|
-
for (var bi = 0; bi < IPV6_BYTE_COUNT; bi++) {
|
|
343
|
-
if (bi < keepBytes) continue;
|
|
344
|
-
if (bi === keepBytes && keepBits > 0) {
|
|
345
|
-
var m = (BYTE_MASK << (IP_BITS_PER_BYTE - keepBits)) & BYTE_MASK;
|
|
346
|
-
bytes[bi] = bytes[bi] & m;
|
|
347
|
-
} else {
|
|
348
|
-
bytes[bi] = 0;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// Re-emit as colon-hex (no compression — deterministic for hashing).
|
|
352
|
-
var out = [];
|
|
353
|
-
for (var oi = 0; oi < IPV6_BYTE_COUNT; oi += 2) {
|
|
354
|
-
out.push(((bytes[oi] << IP_BITS_PER_BYTE) | bytes[oi + 1]).toString(HEX_RADIX));
|
|
355
|
-
}
|
|
356
|
-
return out.join(":") + "/" + prefix;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function _ipPrefix(ip) {
|
|
360
|
-
if (typeof ip !== "string" || ip.length === 0) return "";
|
|
361
|
-
// IPv4-mapped IPv6 (::ffff:1.2.3.4) — strip the wrapper so the v4
|
|
362
|
-
// mask applies. Same bucket regardless of how the proxy reported it.
|
|
363
|
-
var lower = ip.toLowerCase();
|
|
364
|
-
if (lower.indexOf(V4_MAPPED_V6_PREFIX) === 0 && lower.indexOf(".") !== -1) {
|
|
365
|
-
return _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length), IPV4_DEFAULT_PREFIX) || "";
|
|
259
|
+
// public client IP every few requests as the device hops cells, so a strict
|
|
260
|
+
// full-IP fingerprint logs out healthy mobile users. The "clientIpPrefix"
|
|
261
|
+
// field hashes the /24 (IPv4) + /64 (IPv6) subnet bucket instead — drift
|
|
262
|
+
// across the bucket is meaningfully suspicious, drift within is not. The
|
|
263
|
+
// masking lives in requestHelpers.ipPrefix (the IP-utilities home, next to
|
|
264
|
+
// clientIp / trustedClientIp); operators with stricter needs pass a
|
|
265
|
+
// function-form fingerprint field and reuse requestHelpers.ipPrefix for a
|
|
266
|
+
// custom mask width.
|
|
267
|
+
|
|
268
|
+
// Resolve the per-call client-IP function for the clientIp / clientIpPrefix
|
|
269
|
+
// fingerprint fields. With { trustedProxies } (an array/string of CIDRs) or a
|
|
270
|
+
// custom { clientIpResolver }, the IP is peer-gated through
|
|
271
|
+
// requestHelpers.trustedClientIp so a deployment behind a trusted proxy binds
|
|
272
|
+
// the session to the real client and not the proxy address (which silently
|
|
273
|
+
// defeats the IP component of the fingerprint). With neither, it falls back to
|
|
274
|
+
// the bare-socket peer — the historical default, preserved so existing
|
|
275
|
+
// fingerprints don't change and log users out. The SAME option must be passed
|
|
276
|
+
// to create / verify / rotate (exactly like fingerprintFields) or the
|
|
277
|
+
// fingerprint won't match across the session lifecycle. An invalid CIDR throws
|
|
278
|
+
// at the call (config-time entry-point validation).
|
|
279
|
+
function _clientIpResolver(opts) {
|
|
280
|
+
if (opts && (opts.trustedProxies != null || typeof opts.clientIpResolver === "function")) {
|
|
281
|
+
return requestHelpers.trustedClientIp({
|
|
282
|
+
trustedProxies: opts.trustedProxies,
|
|
283
|
+
clientIpResolver: opts.clientIpResolver,
|
|
284
|
+
}).resolve;
|
|
366
285
|
}
|
|
367
|
-
|
|
368
|
-
if (ip.indexOf(".") !== -1) return _maskIpv4(ip, IPV4_DEFAULT_PREFIX) || "";
|
|
369
|
-
return "";
|
|
286
|
+
return requestHelpers.clientIp;
|
|
370
287
|
}
|
|
371
288
|
|
|
372
|
-
function _buildFingerprintInputs(req, fields) {
|
|
289
|
+
function _buildFingerprintInputs(req, fields, resolveIp) {
|
|
373
290
|
if (!req) return null;
|
|
291
|
+
resolveIp = resolveIp || requestHelpers.clientIp;
|
|
374
292
|
var headers = req.headers || {};
|
|
375
293
|
var inputs = {};
|
|
376
294
|
for (var i = 0; i < fields.length; i++) {
|
|
377
295
|
var f = fields[i];
|
|
378
296
|
if (f === "clientIp") {
|
|
379
|
-
inputs.clientIp =
|
|
297
|
+
inputs.clientIp = resolveIp(req) || "";
|
|
380
298
|
} else if (f === "clientIpPrefix") {
|
|
381
|
-
// /24 v4 + /64 v6 — see
|
|
382
|
-
inputs.clientIpPrefix =
|
|
299
|
+
// /24 v4 + /64 v6 — see requestHelpers.ipPrefix commentary.
|
|
300
|
+
inputs.clientIpPrefix = requestHelpers.ipPrefix(resolveIp(req) || "");
|
|
383
301
|
} else if (f === "userAgent") {
|
|
384
302
|
inputs.userAgent = String(headers["user-agent"] || "");
|
|
385
303
|
} else if (f === "acceptLanguage") {
|
|
@@ -482,7 +400,7 @@ async function create(opts) {
|
|
|
482
400
|
var dataObj = opts.data ? Object.assign({}, opts.data) : null;
|
|
483
401
|
var fpFields = Array.isArray(opts.fingerprintFields) && opts.fingerprintFields.length > 0
|
|
484
402
|
? opts.fingerprintFields : DEFAULT_FINGERPRINT_FIELDS;
|
|
485
|
-
var fpInputs = _buildFingerprintInputs(opts.req, fpFields);
|
|
403
|
+
var fpInputs = _buildFingerprintInputs(opts.req, fpFields, _clientIpResolver(opts));
|
|
486
404
|
if (fpInputs) {
|
|
487
405
|
if (!dataObj) dataObj = {};
|
|
488
406
|
dataObj.__bj_fingerprint = _hashFingerprint(sid, fpInputs);
|
|
@@ -660,7 +578,7 @@ async function verify(token, verifyOpts) {
|
|
|
660
578
|
if (storedFingerprint && verifyOpts.req) {
|
|
661
579
|
var fpFields = Array.isArray(verifyOpts.fingerprintFields) && verifyOpts.fingerprintFields.length > 0
|
|
662
580
|
? verifyOpts.fingerprintFields : DEFAULT_FINGERPRINT_FIELDS;
|
|
663
|
-
var currentInputs = _buildFingerprintInputs(verifyOpts.req, fpFields);
|
|
581
|
+
var currentInputs = _buildFingerprintInputs(verifyOpts.req, fpFields, _clientIpResolver(verifyOpts));
|
|
664
582
|
var currentHash = _hashFingerprint(sid, currentInputs);
|
|
665
583
|
if (currentHash !== storedFingerprint) {
|
|
666
584
|
fingerprintDrift = true;
|
|
@@ -1058,7 +976,7 @@ async function rotate(oldToken, opts) {
|
|
|
1058
976
|
"so the device binding can be re-keyed to the new session id", true);
|
|
1059
977
|
}
|
|
1060
978
|
if (!newDataObj) newDataObj = {};
|
|
1061
|
-
newDataObj.__bj_fingerprint = _hashFingerprint(newSid, _buildFingerprintInputs(opts.req, fpFields));
|
|
979
|
+
newDataObj.__bj_fingerprint = _hashFingerprint(newSid, _buildFingerprintInputs(opts.req, fpFields, _clientIpResolver(opts)));
|
|
1062
980
|
}
|
|
1063
981
|
|
|
1064
982
|
var dataJson = newDataObj ? JSON.stringify(newDataObj) : null;
|
|
@@ -798,6 +798,17 @@ class Predicate {
|
|
|
798
798
|
return this._add(joiner, qc + " " + op + " NULL", []);
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
+
// `col = NULL` / `col != NULL` is UNKNOWN in SQL — never true. Emitting it
|
|
802
|
+
// (e.g. from where({ col: null })) silently matches zero rows; worse, a null
|
|
803
|
+
// accidentally passed where a real value was expected (where({ ownerId }))
|
|
804
|
+
// would, if rewritten to `IS NULL`, return orphan rows — an authorization
|
|
805
|
+
// footgun. Refuse it and direct the caller to the explicit NULL predicates.
|
|
806
|
+
if (value === null && (op === "=" || op === "!=" || op === "<>")) {
|
|
807
|
+
throw _err("where(" + JSON.stringify(col) + ", '" + op + "', null) is never true in SQL " +
|
|
808
|
+
"(col " + op + " NULL is UNKNOWN); use whereNull(col) / whereNotNull(col) to test for NULL",
|
|
809
|
+
"sql-builder/null-equality");
|
|
810
|
+
}
|
|
811
|
+
|
|
801
812
|
if ((op === "LIKE" || op === "NOT LIKE") && typeof value === "string") {
|
|
802
813
|
return this._add(joiner, qc + " " + op + " ? ESCAPE '~'", [_escapeLike(value)]);
|
|
803
814
|
}
|
|
@@ -948,6 +959,17 @@ class Predicate {
|
|
|
948
959
|
if (!Array.isArray(values) || values.length === 0) {
|
|
949
960
|
throw _err("whereInArray requires a non-empty array of values", "sql-builder/empty-in");
|
|
950
961
|
}
|
|
962
|
+
// Validate each element is a bindable parameter. On the non-Postgres IN-list
|
|
963
|
+
// path every element is its own `?`, so the driver rejects an undefined at
|
|
964
|
+
// execute; the Postgres `= ANY(?)` path binds the WHOLE array as one param,
|
|
965
|
+
// where an undefined is silently coerced to NULL — diverging per dialect.
|
|
966
|
+
// Reject undefined here so every backend fails the same way, at build.
|
|
967
|
+
for (var vi = 0; vi < values.length; vi += 1) {
|
|
968
|
+
if (values[vi] === undefined) {
|
|
969
|
+
throw _err("whereInArray value[" + vi + "] is undefined (not a bindable parameter)",
|
|
970
|
+
"sql-builder/bad-in-value");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
951
973
|
this._gate(col);
|
|
952
974
|
var qc = _qualifiedColumn(col, this._dialect());
|
|
953
975
|
if (this._dialect() === "postgres") {
|
|
@@ -585,6 +585,7 @@ class WsClient extends EventEmitter {
|
|
|
585
585
|
this._reconnectAttempt = 0;
|
|
586
586
|
this._fragmentChunks = [];
|
|
587
587
|
this._fragmentOpcode = null;
|
|
588
|
+
this._fragmentBytes = 0;
|
|
588
589
|
|
|
589
590
|
this._startHeartbeat();
|
|
590
591
|
if (this._opts.auditOn) {
|
|
@@ -623,6 +624,14 @@ class WsClient extends EventEmitter {
|
|
|
623
624
|
}
|
|
624
625
|
|
|
625
626
|
_handleFrame(frame) {
|
|
627
|
+
// Once the connection has been torn down (e.g. a prior frame in the same
|
|
628
|
+
// parsed batch tripped maxMessageBytes and called _teardown, which sets
|
|
629
|
+
// _closed synchronously), drop any remaining buffered frames — processing
|
|
630
|
+
// them would emit a spurious cascade of protocol errors (a stray
|
|
631
|
+
// continuation after the fragment state reset). A graceful close keeps
|
|
632
|
+
// _closed false until the handshake completes, so the peer's CLOSE frame
|
|
633
|
+
// is still processed and the normal close code surfaces.
|
|
634
|
+
if (this._closed) return;
|
|
626
635
|
// RFC 6455 §5.5: control frames MUST be <= 125 bytes AND non-fragmented.
|
|
627
636
|
var isControl = frame.opcode === OPCODE_PING ||
|
|
628
637
|
frame.opcode === OPCODE_PONG ||
|
|
@@ -692,6 +701,7 @@ class WsClient extends EventEmitter {
|
|
|
692
701
|
this._fragmentOpcode = frame.opcode;
|
|
693
702
|
this._fragmentRsv1 = frame.rsv1 === true;
|
|
694
703
|
this._fragmentChunks = [frame.payload];
|
|
704
|
+
this._fragmentBytes = safeBuffer.byteLengthOf(frame.payload);
|
|
695
705
|
} else if (frame.opcode === OPCODE_CONT) {
|
|
696
706
|
if (this._fragmentOpcode == null) {
|
|
697
707
|
this._handleSocketError(new WsClientError("ws-client/protocol-error",
|
|
@@ -699,6 +709,21 @@ class WsClient extends EventEmitter {
|
|
|
699
709
|
return;
|
|
700
710
|
}
|
|
701
711
|
this._fragmentChunks.push(frame.payload);
|
|
712
|
+
this._fragmentBytes += safeBuffer.byteLengthOf(frame.payload);
|
|
713
|
+
}
|
|
714
|
+
// Enforce maxMessageBytes on the RUNNING fragment total, not only at FIN:
|
|
715
|
+
// a peer that streams continuation frames and never sets FIN would
|
|
716
|
+
// otherwise grow _fragmentChunks without bound, one maxFrameBytes-sized
|
|
717
|
+
// frame at a time (CWE-770 / CWE-400). The per-frame parser cap bounds a
|
|
718
|
+
// single frame, never the sum.
|
|
719
|
+
if (this._fragmentOpcode != null && this._fragmentBytes > this._opts.maxMessageBytes) {
|
|
720
|
+
this._fragmentChunks = [];
|
|
721
|
+
this._fragmentOpcode = null;
|
|
722
|
+
this._fragmentRsv1 = false;
|
|
723
|
+
this._fragmentBytes = 0;
|
|
724
|
+
this._handleSocketError(new WsClientError("ws-client/message-too-big",
|
|
725
|
+
"incoming message exceeds maxMessageBytes (" + this._opts.maxMessageBytes + ")"));
|
|
726
|
+
return;
|
|
702
727
|
}
|
|
703
728
|
if (frame.fin) {
|
|
704
729
|
var fullPayload = Buffer.concat(this._fragmentChunks); // allow:handrolled-buffer-collect — bounded by maxMessageBytes below
|
|
@@ -712,6 +737,7 @@ class WsClient extends EventEmitter {
|
|
|
712
737
|
this._fragmentChunks = [];
|
|
713
738
|
this._fragmentOpcode = null;
|
|
714
739
|
this._fragmentRsv1 = false;
|
|
740
|
+
this._fragmentBytes = 0;
|
|
715
741
|
if (this._negotiatedDeflate && firstFrameRsv1) {
|
|
716
742
|
try {
|
|
717
743
|
var zlib = require("node:zlib"); // allow:inline-require — zlib only on deflate-negotiated path
|
|
@@ -1,33 +1,80 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.x509Chain
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title X.509 chain (CA-bit issuer test)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* The basicConstraints-enforcing issuer test the framework's own
|
|
9
|
+
* certificate-chain walkers route through (<code>b.tsa.verifyToken</code>,
|
|
10
|
+
* <code>b.mail.bimi</code> VMC/CMC, <code>b.mail.crypto.smime</code>,
|
|
11
|
+
* <code>b.mdoc</code>, <code>b.contentCredentials</code>,
|
|
12
|
+
* <code>b.auth.fido</code>). It exists because node:crypto's
|
|
13
|
+
* <code>X509Certificate.checkIssued()</code> validates the issuer/subject
|
|
14
|
+
* DN match, the AKI/SKI linkage, and — only when a keyUsage extension is
|
|
15
|
+
* present — keyCertSign, but it does <strong>not</strong> enforce
|
|
16
|
+
* basicConstraints cA:TRUE. A leaf / end-entity certificate (cA:FALSE)
|
|
17
|
+
* that omits keyUsage is therefore wrongly accepted as a signing CA for
|
|
18
|
+
* the next certificate in the chain — the classic basicConstraints bypass
|
|
19
|
+
* (CVE-2002-0862 class). Every in-tree walker routes its issuer test
|
|
20
|
+
* through these helpers so the cA enforcement can never be forgotten in
|
|
21
|
+
* one walker but present in another.
|
|
22
|
+
*
|
|
23
|
+
* Exposed so a consumer validating an X.509 chain <em>outside</em> a TLS
|
|
24
|
+
* handshake — an operator-uploaded CA bundle, a non-handshake PQ-signed
|
|
25
|
+
* certificate — has the same hardened, fail-closed test instead of being
|
|
26
|
+
* pushed toward the raw <code>checkIssued()</code> path this module
|
|
27
|
+
* exists to prevent. Both helpers fail closed: any malformed input or
|
|
28
|
+
* unsupported key type returns false rather than throwing.
|
|
29
|
+
*
|
|
30
|
+
* @card
|
|
31
|
+
* basicConstraints cA:TRUE-enforcing X.509 issuer test, fail-closed —
|
|
32
|
+
* the hardened alternative to node's checkIssued() for chains built
|
|
33
|
+
* outside a TLS handshake.
|
|
34
|
+
*/
|
|
2
35
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @primitive b.x509Chain.isCaCert
|
|
38
|
+
* @signature b.x509Chain.isCaCert(cert)
|
|
39
|
+
* @since 0.15.15
|
|
40
|
+
* @status stable
|
|
41
|
+
* @related b.x509Chain.issuerValidlyIssued
|
|
42
|
+
*
|
|
43
|
+
* True only when <code>cert</code> asserts basicConstraints cA:TRUE.
|
|
44
|
+
* node's <code>X509Certificate</code> exposes <code>.ca</code> (a boolean);
|
|
45
|
+
* a certificate with no basicConstraints extension or with cA:FALSE
|
|
46
|
+
* returns false. A missing cert or a non-boolean <code>.ca</code> (parse
|
|
47
|
+
* failure / unsupported runtime) fails closed to false.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* var crypto = require("crypto");
|
|
51
|
+
* var ca = new crypto.X509Certificate(caPem);
|
|
52
|
+
* b.x509Chain.isCaCert(ca); // → true only if basicConstraints cA:TRUE
|
|
53
|
+
*/
|
|
21
54
|
function isCaCert(cert) {
|
|
22
55
|
return !!cert && cert.ca === true;
|
|
23
56
|
}
|
|
24
57
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
58
|
+
/**
|
|
59
|
+
* @primitive b.x509Chain.issuerValidlyIssued
|
|
60
|
+
* @signature b.x509Chain.issuerValidlyIssued(issuer, subject)
|
|
61
|
+
* @since 0.15.15
|
|
62
|
+
* @status stable
|
|
63
|
+
* @related b.x509Chain.isCaCert
|
|
64
|
+
*
|
|
65
|
+
* True when <code>issuer</code> validly issued <code>subject</code> AND is
|
|
66
|
+
* itself a CA: the DN / AKI-SKI / keyUsage linkage (checkIssued), the
|
|
67
|
+
* cryptographic signature (verify), and basicConstraints cA:TRUE
|
|
68
|
+
* (isCaCert). The cA check runs first so a non-CA certificate is rejected
|
|
69
|
+
* before the expensive signature verification. Any exception (malformed
|
|
70
|
+
* cert, unsupported key type) fails closed to false.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* var crypto = require("crypto");
|
|
74
|
+
* var issuer = new crypto.X509Certificate(issuerPem);
|
|
75
|
+
* var subject = new crypto.X509Certificate(leafPem);
|
|
76
|
+
* b.x509Chain.issuerValidlyIssued(issuer, subject); // → boolean
|
|
77
|
+
*/
|
|
31
78
|
function issuerValidlyIssued(issuer, subject) {
|
|
32
79
|
try {
|
|
33
80
|
return isCaCert(issuer) &&
|