@blamejs/blamejs-shop 0.4.74 → 0.4.76
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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/customers.js +30 -0
- package/lib/storefront.js +150 -40
- 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
|
@@ -253,6 +253,77 @@ async function testFinalNextFallbackOnUnhandledError() {
|
|
|
253
253
|
capturedErr === sentinel);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
async function testHaltingMiddlewareSettlesPromise() {
|
|
257
|
+
// A regular (3-arg) middleware that writes the response and returns
|
|
258
|
+
// WITHOUT calling next() halts the chain — the documented "this
|
|
259
|
+
// middleware handled the request" pattern (auth/rate-limit/bot block).
|
|
260
|
+
// The composed promise MUST still settle; a permanently-pending
|
|
261
|
+
// promise retains its req/res closure forever (memory leak under
|
|
262
|
+
// sustained blocked traffic). finalNext must NOT fire — the chain was
|
|
263
|
+
// halted, so the caller's next-flag stays false and the router does
|
|
264
|
+
// not proceed to the route handler.
|
|
265
|
+
var finalCalled = false;
|
|
266
|
+
var pipe = b.middleware.composePipeline([
|
|
267
|
+
{ name: "pass", mw: _passMw("a") },
|
|
268
|
+
{ name: "halt", mw: function (req, res, next) { res.writableEnded = true; /* ended response, no next() */ } },
|
|
269
|
+
{ name: "after", mw: _passMw("z") },
|
|
270
|
+
]);
|
|
271
|
+
var req = {}; var res = {};
|
|
272
|
+
var settled = false;
|
|
273
|
+
pipe(req, res, function () { finalCalled = true; }).then(function () { settled = true; });
|
|
274
|
+
try {
|
|
275
|
+
await helpers.waitUntil(function () { return settled; },
|
|
276
|
+
{ timeoutMs: 2000, label: "compose-pipeline: halting middleware settles the composed promise" });
|
|
277
|
+
} catch (_e) { /* stays false on the buggy tree → check fails RED */ }
|
|
278
|
+
check("halting middleware settles the composed promise", settled === true);
|
|
279
|
+
check("downstream middleware not run after a halt",
|
|
280
|
+
!req._tags || req._tags.indexOf("z") === -1);
|
|
281
|
+
check("finalNext NOT called when a middleware halts the chain", finalCalled === false);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function testHandledErrorSettlesWithoutFinalNext() {
|
|
285
|
+
// An error handler that consumes the error WITHOUT calling next has
|
|
286
|
+
// handled the request (same halt contract as a 3-arg middleware): the
|
|
287
|
+
// promise settles but finalNext must not fire, so the caller does not
|
|
288
|
+
// proceed to the route handler on top of an already-sent error page.
|
|
289
|
+
var finalCalled = false;
|
|
290
|
+
var sentinel = new Error("boom");
|
|
291
|
+
var pipe = b.middleware.composePipeline([
|
|
292
|
+
{ name: "failing", mw: _bailMw(sentinel), position: 10 },
|
|
293
|
+
{ name: "errorHandler", mw: function (err, req, res, _next) { res._handled = err; res.writableEnded = true; }, position: 20 },
|
|
294
|
+
]);
|
|
295
|
+
var req = {}; var res = {};
|
|
296
|
+
var settled = false;
|
|
297
|
+
pipe(req, res, function () { finalCalled = true; }).then(function () { settled = true; });
|
|
298
|
+
try {
|
|
299
|
+
await helpers.waitUntil(function () { return settled; },
|
|
300
|
+
{ timeoutMs: 2000, label: "compose-pipeline: handled error settles the composed promise" });
|
|
301
|
+
} catch (_e) { /* RED if it hangs */ }
|
|
302
|
+
check("handled error settles the composed promise", settled === true);
|
|
303
|
+
check("error handler consumed the error", res._handled === sentinel);
|
|
304
|
+
check("finalNext NOT called when an error handler halts the chain", finalCalled === false);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function testDeferredNextContinuesChain() {
|
|
308
|
+
// A callback-style middleware that calls next() LATER (from a timer, stream,
|
|
309
|
+
// or legacy callback) returns before next() runs. The pipeline must NOT treat
|
|
310
|
+
// that bare return as a halt: the chain continues when the deferred next()
|
|
311
|
+
// fires, downstream middleware run, and finalNext is called.
|
|
312
|
+
var finalCalled = false;
|
|
313
|
+
var pipe = b.middleware.composePipeline([
|
|
314
|
+
{ name: "a", mw: _passMw("a") },
|
|
315
|
+
{ name: "deferred", mw: function (req, res, next) {
|
|
316
|
+
setTimeout(function () { req._tags.push("deferred"); next(); }, 5);
|
|
317
|
+
} },
|
|
318
|
+
{ name: "c", mw: _passMw("c") },
|
|
319
|
+
]);
|
|
320
|
+
var req = {}; var res = {};
|
|
321
|
+
await pipe(req, res, function (err) { finalCalled = !err; });
|
|
322
|
+
check("deferred next() continues the chain (callback-style middleware)",
|
|
323
|
+
req._tags && req._tags.join("") === "adeferredc");
|
|
324
|
+
check("finalNext called after a deferred next()", finalCalled === true);
|
|
325
|
+
}
|
|
326
|
+
|
|
256
327
|
async function run() {
|
|
257
328
|
testSurface();
|
|
258
329
|
testSequentialDispatch();
|
|
@@ -269,6 +340,9 @@ async function run() {
|
|
|
269
340
|
await testAsyncMiddlewareAwaited();
|
|
270
341
|
await testErrorMiddlewareReceivesError();
|
|
271
342
|
await testFinalNextFallbackOnUnhandledError();
|
|
343
|
+
await testHaltingMiddlewareSettlesPromise();
|
|
344
|
+
await testHandledErrorSettlesWithoutFinalNext();
|
|
345
|
+
await testDeferredNextContinuesChain();
|
|
272
346
|
}
|
|
273
347
|
|
|
274
348
|
module.exports = { run: run };
|
|
@@ -95,6 +95,18 @@ function run() {
|
|
|
95
95
|
midBand.mechanismRequired === "standard-contract" &&
|
|
96
96
|
midBand.securityAssessmentRequired === false);
|
|
97
97
|
|
|
98
|
+
// Same band reached via the cumulativePI field: 100,001 prior + 50,000 this
|
|
99
|
+
// transfer = 150,001 effective (in the 100k–1M band) is still SCC — the
|
|
100
|
+
// documented boundary is 1,000,000, not 100,000.
|
|
101
|
+
var cumMid = b.pipl.sccFilingAssessment({
|
|
102
|
+
assessmentId: "xfer-3d", transferType: "processor", recipientJurisdiction: "US",
|
|
103
|
+
dataCategories: ["contact"], legalBasis: "standard-contract",
|
|
104
|
+
volume: 50000, sensitivePI: false, cumulativePI: 100001, recordedAt: recordedAt,
|
|
105
|
+
});
|
|
106
|
+
check("scc: 150,001 cumulative non-sensitive stays standard-contract (boundary is 1M)",
|
|
107
|
+
cumMid.mechanismRequired === "standard-contract" &&
|
|
108
|
+
cumMid.securityAssessmentRequired === false);
|
|
109
|
+
|
|
98
110
|
// ---- THIS transfer's volume counts toward the cumulative sensitive
|
|
99
111
|
// threshold: a first transfer of 10,001 sensitive subjects forces it
|
|
100
112
|
// even with cumulativeSensitivePI omitted (defaults 0) ----
|
|
@@ -133,6 +133,27 @@ function testParseListHeader() {
|
|
|
133
133
|
JSON.stringify(rh.parseListHeader("\ta\t,\tb\n")) === '["a","b"]');
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function testParseQualityListQuoteAware() {
|
|
137
|
+
var rh = b.requestHelpers;
|
|
138
|
+
// The q-value must come from the parameter literally named `q`, parsed
|
|
139
|
+
// quote-aware — never a `q=`-shaped substring inside a quoted parameter value,
|
|
140
|
+
// and a quoted value's ',' / ';' must not split the list.
|
|
141
|
+
var quoted = rh.parseQualityList('text/html;title="x;q=0.1";q=0.9');
|
|
142
|
+
check("parseQualityList: q= inside a quoted value is not the q-value",
|
|
143
|
+
quoted.length === 1 && quoted[0].value === "text/html" && quoted[0].q === 0.9);
|
|
144
|
+
var commaInQuote = rh.parseQualityList('a/b;p="x,y";q=0.3');
|
|
145
|
+
check("parseQualityList: comma inside a quoted value does not split the list",
|
|
146
|
+
commaInQuote.length === 1 && commaInQuote[0].value === "a/b" && commaInQuote[0].q === 0.3);
|
|
147
|
+
var leveled = rh.parseQualityList("text/html;level=1;q=0.5");
|
|
148
|
+
check("parseQualityList: a media-type param before q is ignored",
|
|
149
|
+
leveled.length === 1 && leveled[0].q === 0.5);
|
|
150
|
+
var ranked = rh.parseQualityList("br;q=1.0, gzip;q=0.5, *;q=0");
|
|
151
|
+
check("parseQualityList: ranks by descending q",
|
|
152
|
+
ranked[0].value === "br" && ranked[1].value === "gzip" && ranked[2].q === 0);
|
|
153
|
+
check("parseQualityList: missing q defaults to 1",
|
|
154
|
+
rh.parseQualityList("en")[0].q === 1);
|
|
155
|
+
}
|
|
156
|
+
|
|
136
157
|
function testSafeHeadersDistinct() {
|
|
137
158
|
check("safeHeadersDistinct is fn", typeof b.requestHelpers.safeHeadersDistinct === "function");
|
|
138
159
|
|
|
@@ -416,9 +437,33 @@ function testTrustedClientIpValidates() {
|
|
|
416
437
|
check("trustedClientIp rejects malformed CIDR", threwCidr === true);
|
|
417
438
|
}
|
|
418
439
|
|
|
440
|
+
function testIpPrefixMasking() {
|
|
441
|
+
var ip = b.requestHelpers.ipPrefix;
|
|
442
|
+
check("ipPrefix is a function", typeof ip === "function");
|
|
443
|
+
// IPv4 → /24 (network address, low octet zeroed).
|
|
444
|
+
check("ipPrefix v4 masks to /24", ip("203.0.113.47") === "203.0.113.0/24");
|
|
445
|
+
check("ipPrefix v4 same /24 → same bucket", ip("203.0.113.47") === ip("203.0.113.250"));
|
|
446
|
+
check("ipPrefix v4 cross-/24 → different bucket", ip("203.0.113.1") !== ip("198.51.100.1"));
|
|
447
|
+
// IPv6 → /64 (low 64 bits zeroed), deterministic uncompressed emit.
|
|
448
|
+
check("ipPrefix v6 masks to /64", ip("2001:db8:1234:5678::1") === "2001:db8:1234:5678:0:0:0:0/64");
|
|
449
|
+
check("ipPrefix v6 same /64 → same bucket",
|
|
450
|
+
ip("2001:db8:1234:5678::1") === ip("2001:db8:1234:5678:abcd:ef01:2345:6789"));
|
|
451
|
+
check("ipPrefix v6 cross-/64 → different bucket",
|
|
452
|
+
ip("2001:db8:1234:5678::1") !== ip("2001:db8:1234:9999::1"));
|
|
453
|
+
// IPv4-mapped IPv6 folds to the v4 /24 bucket.
|
|
454
|
+
check("ipPrefix folds ::ffff: mapped v4 to the v4 bucket",
|
|
455
|
+
ip("::ffff:203.0.113.5") === ip("203.0.113.99"));
|
|
456
|
+
// Garbage / non-string → "" (never throws).
|
|
457
|
+
check("ipPrefix returns '' for a non-string", ip(null) === "" && ip(12345) === "");
|
|
458
|
+
check("ipPrefix returns '' for an empty string", ip("") === "");
|
|
459
|
+
check("ipPrefix returns '' for an unparseable address", ip("not-an-ip") === "");
|
|
460
|
+
check("ipPrefix rejects an out-of-range v4 octet", ip("999.0.0.1") === "");
|
|
461
|
+
}
|
|
462
|
+
|
|
419
463
|
async function run() {
|
|
420
464
|
testSurface();
|
|
421
465
|
testSafeHeadersDistinct();
|
|
466
|
+
testIpPrefixMasking();
|
|
422
467
|
testClientIpDefaultIgnoresXff();
|
|
423
468
|
testClientIpPeerGatedTrustedPeer();
|
|
424
469
|
testClientIpPeerGatedUntrustedPeerIgnoresXff();
|
|
@@ -439,6 +484,7 @@ async function run() {
|
|
|
439
484
|
await testCaptureStatusOnEndThrowDoesntBreakResponse();
|
|
440
485
|
testCaptureStatusValidatesArgs();
|
|
441
486
|
testParseListHeader();
|
|
487
|
+
testParseQualityListQuoteAware();
|
|
442
488
|
testExtractBearerSurface();
|
|
443
489
|
testExtractBearerHappyPath();
|
|
444
490
|
testExtractBearerCaseInsensitiveScheme();
|
package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js
CHANGED
|
@@ -164,6 +164,21 @@ function _hokScd(notOnOrAfterAttr, holderCertPem) {
|
|
|
164
164
|
"</saml:SubjectConfirmationData>";
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// Same shape, Recipient attribute OMITTED — the Web SSO profile (§4.1.4.2)
|
|
168
|
+
// makes Recipient mandatory for a Bearer/HoK confirmation delivered to an ACS.
|
|
169
|
+
function _bearerScdNoRecipient(notOnOrAfterAttr, inResponseTo) {
|
|
170
|
+
return "<saml:SubjectConfirmationData" + notOnOrAfterAttr +
|
|
171
|
+
" InResponseTo=\"" + inResponseTo + "\"/>";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _hokScdNoRecipient(notOnOrAfterAttr, holderCertPem) {
|
|
175
|
+
return "<saml:SubjectConfirmationData" + notOnOrAfterAttr + ">" +
|
|
176
|
+
"<ds:KeyInfo xmlns:ds=\"" + DS + "\"><ds:X509Data><ds:X509Certificate>" +
|
|
177
|
+
_certBodyB64(holderCertPem) +
|
|
178
|
+
"</ds:X509Certificate></ds:X509Data></ds:KeyInfo>" +
|
|
179
|
+
"</saml:SubjectConfirmationData>";
|
|
180
|
+
}
|
|
181
|
+
|
|
167
182
|
function _newSp(idp) {
|
|
168
183
|
return b.auth.saml.sp.create({
|
|
169
184
|
entityId: SP_ENTITY_ID,
|
|
@@ -274,6 +289,41 @@ async function testHolderOfKeyNotOnOrAfterRefused() {
|
|
|
274
289
|
_verifyThrows(sp, b64bad, vopts) === "auth-saml/no-valid-confirmation");
|
|
275
290
|
}
|
|
276
291
|
|
|
292
|
+
// SAML 2.0 Profiles §4.1.4.2 — a Bearer SubjectConfirmationData delivered to
|
|
293
|
+
// an ACS MUST carry a Recipient equal to the SP's ACS URL. An assertion whose
|
|
294
|
+
// Bearer confirmation omits Recipient must be refused; accepting it lets an
|
|
295
|
+
// assertion relayed to an unintended endpoint pass the recipient-binding axis.
|
|
296
|
+
async function testBearerMissingRecipientRefused() {
|
|
297
|
+
var idp = await _mintRsaCert("idp.example");
|
|
298
|
+
var sp = _newSp(idp);
|
|
299
|
+
var inResponseTo = "_req-no-recip";
|
|
300
|
+
var b64 = _buildSignedResponse(idp, {
|
|
301
|
+
tag: "bearer-no-recip",
|
|
302
|
+
method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
|
|
303
|
+
nameId: "alice@example.com",
|
|
304
|
+
scd: _bearerScdNoRecipient(" NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\"", inResponseTo), // allow:raw-time-literal — 5m future
|
|
305
|
+
});
|
|
306
|
+
check("Bearer with no Recipient is refused (§4.1.4.2 mandatory)",
|
|
307
|
+
_verifyThrows(sp, b64, { expectedInResponseTo: inResponseTo }) === "auth-saml/no-valid-confirmation");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The Holder-of-Key sibling incorporates the same Web SSO Recipient requirement
|
|
311
|
+
// (Profile §3.1 by reference). An HoK confirmation delivered to an ACS with no
|
|
312
|
+
// Recipient must be refused too.
|
|
313
|
+
async function testHolderOfKeyMissingRecipientRefused() {
|
|
314
|
+
var idp = await _mintRsaCert("idp.example");
|
|
315
|
+
var holder = await _mintRsaCert("holder.example");
|
|
316
|
+
var sp = _newSp(idp);
|
|
317
|
+
var b64 = _buildSignedResponse(idp, {
|
|
318
|
+
tag: "hok-no-recip",
|
|
319
|
+
method: "urn:oasis:names:tc:SAML:2.0:cm:holder-of-key",
|
|
320
|
+
nameId: "bob@example.com",
|
|
321
|
+
scd: _hokScdNoRecipient(" NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\"", holder.certPem), // allow:raw-time-literal — 5m future
|
|
322
|
+
});
|
|
323
|
+
check("HoK with no Recipient is refused (§3.1 incorporates §4.1.4.2)",
|
|
324
|
+
_verifyThrows(sp, b64, { holderOfKey: { presentedCertPem: holder.certPem } }) === "auth-saml/no-valid-confirmation");
|
|
325
|
+
}
|
|
326
|
+
|
|
277
327
|
// #B0 — a signed assertion with NO AudienceRestriction is not bound to THIS
|
|
278
328
|
// SP. Accepting it is audience-confusion: an IdP-signed assertion minted for
|
|
279
329
|
// another SP is replayed here. Must fail closed (default on).
|
|
@@ -314,12 +364,39 @@ async function testUnparseableConditionsTimestampRefused() {
|
|
|
314
364
|
_verifyThrows(sp, b64, { expectedInResponseTo: inResponseTo }) === "auth-saml/conditions-bad-timestamp");
|
|
315
365
|
}
|
|
316
366
|
|
|
367
|
+
// SAML core §2.5.1.4 — multiple <AudienceRestriction> elements are AND-combined:
|
|
368
|
+
// the SP must be a member of EVERY one. An assertion whose first restriction
|
|
369
|
+
// lists this SP but whose second narrows to a DIFFERENT audience must be refused
|
|
370
|
+
// (checking only the first let it through — audience-confusion).
|
|
371
|
+
async function testSecondAudienceRestrictionEnforced() {
|
|
372
|
+
var idp = await _mintRsaCert("idp.example");
|
|
373
|
+
var sp = _newSp(idp);
|
|
374
|
+
var inResponseTo = "_req-2aud";
|
|
375
|
+
var b64 = _buildSignedResponse(idp, {
|
|
376
|
+
tag: "two-aud",
|
|
377
|
+
method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
|
|
378
|
+
nameId: "u@example.com",
|
|
379
|
+
scd: _bearerScd(" NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\"", inResponseTo), // allow:raw-time-literal — 5m future
|
|
380
|
+
conditions: "<saml:Conditions NotBefore=\"" + _isoFromNow(-5 * 60 * 1000) + // allow:raw-time-literal — 5m skew
|
|
381
|
+
"\" NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\">" + // allow:raw-time-literal — 5m future
|
|
382
|
+
"<saml:AudienceRestriction><saml:Audience>" + SP_ENTITY_ID +
|
|
383
|
+
"</saml:Audience></saml:AudienceRestriction>" +
|
|
384
|
+
"<saml:AudienceRestriction><saml:Audience>https://other-sp.example/different" +
|
|
385
|
+
"</saml:Audience></saml:AudienceRestriction></saml:Conditions>",
|
|
386
|
+
});
|
|
387
|
+
check("second AudienceRestriction (different audience) refused (AND-combined)",
|
|
388
|
+
_verifyThrows(sp, b64, { expectedInResponseTo: inResponseTo }) === "auth-saml/wrong-audience");
|
|
389
|
+
}
|
|
390
|
+
|
|
317
391
|
async function run() {
|
|
318
392
|
await testBearerValidNotOnOrAfterAccepted();
|
|
319
393
|
await testBearerMissingNotOnOrAfterRefused();
|
|
320
394
|
await testBearerUnparseableNotOnOrAfterRefused();
|
|
321
395
|
await testHolderOfKeyNotOnOrAfterRefused();
|
|
396
|
+
await testBearerMissingRecipientRefused();
|
|
397
|
+
await testHolderOfKeyMissingRecipientRefused();
|
|
322
398
|
await testMissingAudienceRestrictionRefused();
|
|
399
|
+
await testSecondAudienceRestrictionEnforced();
|
|
323
400
|
await testUnparseableConditionsTimestampRefused();
|
|
324
401
|
}
|
|
325
402
|
|
|
@@ -152,6 +152,79 @@ async function testClientIpPrefixV4MappedV6() {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// A request whose immediate peer is a reverse proxy: the socket peer is the
|
|
156
|
+
// proxy, the real client arrives in X-Forwarded-For.
|
|
157
|
+
function _makeProxiedReq(proxyAddr, clientIp) {
|
|
158
|
+
return {
|
|
159
|
+
headers: { "x-forwarded-for": clientIp, "user-agent": "ua1" },
|
|
160
|
+
socket: { remoteAddress: proxyAddr },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function testFingerprintPeerGatedClientIp() {
|
|
165
|
+
// Behind a trusted proxy the client IP arrives in X-Forwarded-For while the
|
|
166
|
+
// socket peer is the proxy. The bare-socket default binds the fingerprint to
|
|
167
|
+
// the PROXY, so two different real clients behind the same proxy share a
|
|
168
|
+
// fingerprint — the IP component is silently defeated. The { trustedProxies }
|
|
169
|
+
// option peer-gates the resolve (consistent with trustedClientIp) so the real
|
|
170
|
+
// client is bound. Both halves are proven here: the default still binds the
|
|
171
|
+
// proxy (a different real client does NOT drift), and the opt makes a
|
|
172
|
+
// different real client DRIFT.
|
|
173
|
+
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ses-peergate-"));
|
|
174
|
+
try {
|
|
175
|
+
await setupTestDb(tmpDir);
|
|
176
|
+
var PROXY = "10.0.0.7";
|
|
177
|
+
var TP = ["10.0.0.0/8"];
|
|
178
|
+
|
|
179
|
+
// --- Legacy default (no trustedProxies): binds to the proxy address, so a
|
|
180
|
+
// different real client behind the same proxy does NOT drift.
|
|
181
|
+
var sLegacy = await b.session.create({
|
|
182
|
+
userId: "u-legacy", req: _makeProxiedReq(PROXY, "203.0.113.10"),
|
|
183
|
+
fingerprintFields: ["clientIp"],
|
|
184
|
+
});
|
|
185
|
+
var legacy = await b.session.verify(sLegacy.token, {
|
|
186
|
+
req: _makeProxiedReq(PROXY, "198.51.100.9"), fingerprintFields: ["clientIp"],
|
|
187
|
+
});
|
|
188
|
+
check("bare-socket default: different real client behind proxy does NOT drift (proxy-bound)",
|
|
189
|
+
legacy && legacy.fingerprintDrift === false);
|
|
190
|
+
|
|
191
|
+
// --- Peer-gated (trustedProxies): resolves the real client from XFF.
|
|
192
|
+
var sGated = await b.session.create({
|
|
193
|
+
userId: "u-gated", req: _makeProxiedReq(PROXY, "203.0.113.10"),
|
|
194
|
+
fingerprintFields: ["clientIp"], trustedProxies: TP,
|
|
195
|
+
});
|
|
196
|
+
var sameClient = await b.session.verify(sGated.token, {
|
|
197
|
+
req: _makeProxiedReq(PROXY, "203.0.113.10"),
|
|
198
|
+
fingerprintFields: ["clientIp"], trustedProxies: TP,
|
|
199
|
+
});
|
|
200
|
+
check("peer-gated: same real client behind proxy does not drift",
|
|
201
|
+
sameClient && sameClient.fingerprintDrift === false);
|
|
202
|
+
var diffClient = await b.session.verify(sGated.token, {
|
|
203
|
+
req: _makeProxiedReq(PROXY, "198.51.100.9"),
|
|
204
|
+
fingerprintFields: ["clientIp"], trustedProxies: TP,
|
|
205
|
+
});
|
|
206
|
+
check("peer-gated: different real client behind proxy DRIFTS (real client bound)",
|
|
207
|
+
diffClient && diffClient.fingerprintDrift === true);
|
|
208
|
+
|
|
209
|
+
// A forged XFF from a NON-trusted peer must be ignored (peer-gating) — the
|
|
210
|
+
// resolve falls back to the untrusted socket address, so a forged header
|
|
211
|
+
// can't make the real-client binding drift on its own.
|
|
212
|
+
var sDirect = await b.session.create({
|
|
213
|
+
userId: "u-direct", req: _makeProxiedReq("203.0.113.50", "203.0.113.10"),
|
|
214
|
+
fingerprintFields: ["clientIp"], trustedProxies: TP,
|
|
215
|
+
});
|
|
216
|
+
var forged = await b.session.verify(sDirect.token, {
|
|
217
|
+
// same untrusted socket peer, attacker varies the forgeable XFF.
|
|
218
|
+
req: _makeProxiedReq("203.0.113.50", "8.8.8.8"),
|
|
219
|
+
fingerprintFields: ["clientIp"], trustedProxies: TP,
|
|
220
|
+
});
|
|
221
|
+
check("peer-gated: forged XFF from an untrusted peer is ignored (no drift on header alone)",
|
|
222
|
+
forged && forged.fingerprintDrift === false);
|
|
223
|
+
} finally {
|
|
224
|
+
await teardownTestDb(tmpDir);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
155
228
|
async function testPluggableStore() {
|
|
156
229
|
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ses-store-"));
|
|
157
230
|
try {
|
|
@@ -403,6 +476,7 @@ async function run() {
|
|
|
403
476
|
await testClientIpPrefixV4();
|
|
404
477
|
await testClientIpPrefixV6();
|
|
405
478
|
await testClientIpPrefixV4MappedV6();
|
|
479
|
+
await testFingerprintPeerGatedClientIp();
|
|
406
480
|
await testPluggableStore();
|
|
407
481
|
await testDestroyAllForUserPluggableNoDb();
|
|
408
482
|
await testPluggableStoreValidation();
|
|
@@ -128,6 +128,25 @@ async function run() {
|
|
|
128
128
|
check("LIKE escapes wildcards with ~", likeq.sql.indexOf("ESCAPE '~'") !== -1 &&
|
|
129
129
|
likeq.params[0].indexOf("~%") !== -1 && likeq.params[0].indexOf("~_") !== -1);
|
|
130
130
|
|
|
131
|
+
// ---- NULL-equality footgun: `col = NULL` is UNKNOWN in SQL ----
|
|
132
|
+
rejects("where(col, '=', null) refused (use whereNull)", function () {
|
|
133
|
+
return sql.select("t").where("c", "=", null).toSql();
|
|
134
|
+
}, "sql-builder/null-equality");
|
|
135
|
+
rejects("where({ col: null }) refused (object form is = null)", function () {
|
|
136
|
+
return sql.select("t").where({ c: null }).toSql();
|
|
137
|
+
}, "sql-builder/null-equality");
|
|
138
|
+
var isNullq = sql.select("t").whereNull("c").toSql();
|
|
139
|
+
check("whereNull emits IS NULL with no bound param",
|
|
140
|
+
isNullq.sql.indexOf('"c" IS NULL') !== -1 && isNullq.params.length === 0);
|
|
141
|
+
|
|
142
|
+
// ---- whereInArray: per-element validation parity across dialects ----
|
|
143
|
+
rejects("whereInArray undefined element refused (PG = ANY would bind it silently)", function () {
|
|
144
|
+
return sql.select("t", { dialect: "postgres" }).whereInArray("id", [1, undefined, 3]).toSql();
|
|
145
|
+
}, "sql-builder/bad-in-value");
|
|
146
|
+
var anyq = sql.select("t", { dialect: "postgres" }).whereInArray("id", [1, 2]).toSql();
|
|
147
|
+
check("whereInArray PG path binds the array as one = ANY(?) param",
|
|
148
|
+
anyq.sql.indexOf("= ANY(?)") !== -1 && anyq.params.length === 1);
|
|
149
|
+
|
|
131
150
|
// ---- JSONB guard + jsonb_exists emission (inherited from db-query) ----
|
|
132
151
|
var jc = sql.select("docs", { dialect: "postgres" }).where("meta", "@>", { a: 1 }).toSql();
|
|
133
152
|
check("@> binds canonical JSON", jc.sql.indexOf('"meta" @> ?') !== -1 && jc.params[0] === '{"a":1}');
|
|
@@ -54,6 +54,19 @@ function _makeServer(opts) {
|
|
|
54
54
|
socket.write(headerLines.join("\r\n"));
|
|
55
55
|
|
|
56
56
|
var ws = b.websocket;
|
|
57
|
+
// Memory-exhaustion mode: stream a text frame + continuation frames with
|
|
58
|
+
// fin:false whose running total exceeds the client's maxMessageBytes, and
|
|
59
|
+
// never send FIN. A client that only checks the cap at FIN would buffer
|
|
60
|
+
// them without bound.
|
|
61
|
+
if (opts.floodFragments) {
|
|
62
|
+
var part = Buffer.alloc(opts.floodFragments.partBytes || 600, 0x61);
|
|
63
|
+
socket.write(ws.serializeFrame(0x01, part, { fin: false })); // text start
|
|
64
|
+
var fcount = opts.floodFragments.count || 4;
|
|
65
|
+
for (var fk = 0; fk < fcount; fk += 1) {
|
|
66
|
+
socket.write(ws.serializeFrame(0x00, part, { fin: false })); // continuation, never FIN
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
57
70
|
var fp = new ws.FrameParser({ maxFrameBytes: 1024 * 1024 });
|
|
58
71
|
socket.on("data", function (chunk) {
|
|
59
72
|
var frames = fp.push(chunk) || [];
|
|
@@ -248,6 +261,25 @@ async function run() {
|
|
|
248
261
|
await _sleep(50);
|
|
249
262
|
server7.close();
|
|
250
263
|
|
|
264
|
+
// ---- maxMessageBytes guard on RECEIVE across non-FIN fragments ----
|
|
265
|
+
// A peer that streams continuation frames and never sends FIN must not be
|
|
266
|
+
// able to grow the reassembly buffer past maxMessageBytes — the cap is
|
|
267
|
+
// enforced on the running fragment total, not only at FIN (CWE-770).
|
|
268
|
+
var serverFlood = await _makeServer({ floodFragments: { partBytes: 600, count: 4 } });
|
|
269
|
+
var portFlood = serverFlood.address().port;
|
|
270
|
+
var cFlood = b.wsClient.connect("ws://127.0.0.1:" + portFlood, {
|
|
271
|
+
maxMessageBytes: 1024, // 600 + 600 = 1200 > 1024 before any FIN
|
|
272
|
+
reconnect: false, audit: false, allowInternal: true,
|
|
273
|
+
});
|
|
274
|
+
var floodErr = null;
|
|
275
|
+
cFlood.on("error", function (e) { floodErr = e; });
|
|
276
|
+
await _sleep(400);
|
|
277
|
+
check("receive: running fragment total over maxMessageBytes errors before FIN",
|
|
278
|
+
floodErr !== null && /maxMessageBytes/.test(floodErr.message || ""));
|
|
279
|
+
try { cFlood.close(); } catch (_e) { /* already torn down */ }
|
|
280
|
+
await _sleep(50);
|
|
281
|
+
serverFlood.close();
|
|
282
|
+
|
|
251
283
|
// ---- url + readyState getters ----
|
|
252
284
|
var server8 = await _makeServer({});
|
|
253
285
|
var port8 = server8.address().port;
|
|
@@ -97,6 +97,17 @@ function _stubHttpClient(body) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
async function run() {
|
|
100
|
+
// ---- 0. Public surface: the CA-bit issuer test is reachable on `b` so a
|
|
101
|
+
// consumer validating a chain outside a TLS handshake uses the hardened,
|
|
102
|
+
// fail-closed test instead of raw X509Certificate.checkIssued().
|
|
103
|
+
check("b.x509Chain is exposed on the public surface", b.x509Chain && typeof b.x509Chain === "object");
|
|
104
|
+
check("b.x509Chain.isCaCert is the internal helper", b.x509Chain.isCaCert === x509Chain.isCaCert);
|
|
105
|
+
check("b.x509Chain.issuerValidlyIssued is the internal helper",
|
|
106
|
+
b.x509Chain.issuerValidlyIssued === x509Chain.issuerValidlyIssued);
|
|
107
|
+
check("b.x509Chain.isCaCert fails closed on a missing cert", b.x509Chain.isCaCert(null) === false);
|
|
108
|
+
check("b.x509Chain.issuerValidlyIssued fails closed on garbage input",
|
|
109
|
+
b.x509Chain.issuerValidlyIssued(null, null) === false);
|
|
110
|
+
|
|
100
111
|
// ---- 1. Shared primitive: the cA enforcement every walker routes through.
|
|
101
112
|
var c = await _mintChain({ interCa: false }); // intermediate is cA:FALSE
|
|
102
113
|
check("isCaCert: root (cA:TRUE) is a CA", x509Chain.isCaCert(c.root) === true);
|
package/package.json
CHANGED