@blamejs/core 0.14.5 → 0.14.7
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/README.md +4 -2
- package/lib/agent-event-bus.js +4 -4
- package/lib/agent-idempotency.js +6 -6
- package/lib/agent-orchestrator.js +9 -9
- package/lib/agent-posture-chain.js +10 -10
- package/lib/agent-saga.js +6 -7
- package/lib/agent-snapshot.js +8 -8
- package/lib/agent-stream.js +3 -3
- package/lib/agent-tenant.js +4 -4
- package/lib/agent-trace.js +5 -5
- package/lib/ai-disclosure.js +3 -3
- package/lib/app.js +2 -2
- package/lib/archive-read.js +1 -1
- package/lib/archive-tar-read.js +1 -1
- package/lib/archive-wrap.js +5 -5
- package/lib/audit-tools.js +65 -5
- package/lib/audit.js +2 -2
- package/lib/auth/ciba.js +1 -1
- package/lib/auth/dpop.js +1 -1
- package/lib/auth/fal.js +1 -1
- package/lib/auth/fido-mds3.js +2 -3
- package/lib/auth/jwt-external.js +2 -2
- package/lib/auth/oauth.js +9 -9
- package/lib/auth/oid4vci.js +7 -7
- package/lib/auth/oid4vp.js +1 -1
- package/lib/auth/openid-federation.js +5 -5
- package/lib/auth/passkey.js +6 -6
- package/lib/auth/saml.js +1 -1
- package/lib/auth/sd-jwt-vc.js +3 -6
- package/lib/backup/index.js +18 -18
- package/lib/cache.js +4 -4
- package/lib/calendar.js +5 -5
- package/lib/circuit-breaker.js +1 -1
- package/lib/cms-codec.js +2 -2
- package/lib/compliance.js +14 -14
- package/lib/cra-report.js +3 -3
- package/lib/crypto-field.js +58 -21
- package/lib/crypto.js +5 -6
- package/lib/db-query.js +131 -9
- package/lib/db.js +106 -22
- package/lib/external-db.js +64 -16
- package/lib/framework-schema.js +4 -4
- package/lib/guard-list-id.js +2 -2
- package/lib/guard-list-unsubscribe.js +1 -2
- package/lib/incident-report.js +150 -0
- package/lib/mail-crypto-smime.js +1 -1
- package/lib/mail-deploy.js +3 -3
- package/lib/mail-server-managesieve.js +2 -2
- package/lib/mail-server-pop3.js +2 -2
- package/lib/mail-store.js +1 -1
- package/lib/metrics.js +8 -8
- package/lib/middleware/age-gate.js +20 -7
- package/lib/middleware/bearer-auth.js +36 -35
- package/lib/middleware/bot-guard.js +17 -5
- package/lib/middleware/cors.js +28 -12
- package/lib/middleware/csrf-protect.js +23 -15
- package/lib/middleware/daily-byte-quota.js +27 -13
- package/lib/middleware/deny-response.js +140 -0
- package/lib/middleware/dpop.js +37 -24
- package/lib/middleware/fetch-metadata.js +21 -12
- package/lib/middleware/host-allowlist.js +19 -8
- package/lib/middleware/idempotency-key.js +21 -22
- package/lib/middleware/index.js +3 -0
- package/lib/middleware/network-allowlist.js +24 -10
- package/lib/middleware/protected-resource-metadata.js +2 -2
- package/lib/middleware/rate-limit.js +22 -5
- package/lib/middleware/require-aal.js +25 -10
- package/lib/middleware/require-auth.js +32 -16
- package/lib/middleware/require-bound-key.js +49 -18
- package/lib/middleware/require-content-type.js +19 -8
- package/lib/middleware/require-methods.js +17 -7
- package/lib/middleware/require-mtls.js +27 -14
- package/lib/network-dns-resolver.js +2 -2
- package/lib/network-dns.js +1 -2
- package/lib/network-tls.js +0 -1
- package/lib/network.js +4 -4
- package/lib/outbox.js +1 -1
- package/lib/pqc-agent.js +1 -1
- package/lib/retention.js +1 -1
- package/lib/retry.js +1 -1
- package/lib/safe-archive.js +2 -2
- package/lib/safe-ical.js +2 -2
- package/lib/safe-mime.js +1 -1
- package/lib/self-update-standalone-verifier.js +1 -1
- package/lib/self-update.js +2 -2
- package/lib/static.js +1 -1
- package/lib/subject.js +2 -2
- package/lib/vault/index.js +64 -1
- package/lib/vault/rotate.js +19 -0
- package/lib/vendor-data.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -62,6 +62,7 @@ var requestHelpers = require("../request-helpers");
|
|
|
62
62
|
var safeAsync = require("../safe-async");
|
|
63
63
|
var validateOpts = require("../validate-opts");
|
|
64
64
|
var clusterStorage = require("../cluster-storage");
|
|
65
|
+
var denyResponse = require("./deny-response").denyResponse;
|
|
65
66
|
|
|
66
67
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
67
68
|
var logger = lazyRequire(function () { return require("../log").boot("rate-limit"); });
|
|
@@ -363,6 +364,8 @@ function _resolveBackend(opts) {
|
|
|
363
364
|
* keyFn: function(req): string,
|
|
364
365
|
* statusOnLimit: number, // default 429
|
|
365
366
|
* bodyOnLimit: string, // default "Too Many Requests"
|
|
367
|
+
* onDeny: function(req, res, info): void, // own the refusal response; info = { status, reason, limit, remaining, retryAfter, key }
|
|
368
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
|
|
366
369
|
* header: boolean, // default true
|
|
367
370
|
* headerPrefix: string, // default "X-RateLimit-" — builds <prefix>Limit / <prefix>Remaining (e.g. "RateLimit-" for the IETF draft names)
|
|
368
371
|
* skipPaths: Array<string|RegExp>,
|
|
@@ -391,7 +394,8 @@ function _resolveBackend(opts) {
|
|
|
391
394
|
function create(opts) {
|
|
392
395
|
opts = opts || {};
|
|
393
396
|
validateOpts(opts, [
|
|
394
|
-
"keyFn", "statusOnLimit", "bodyOnLimit", "
|
|
397
|
+
"keyFn", "statusOnLimit", "bodyOnLimit", "onDeny", "problemDetails",
|
|
398
|
+
"header", "headerPrefix", "skipPaths", "scope",
|
|
395
399
|
"backend", "trustProxy", "algorithm",
|
|
396
400
|
// memory backend (token-bucket)
|
|
397
401
|
"burst", "refillPerSecond",
|
|
@@ -404,6 +408,8 @@ function create(opts) {
|
|
|
404
408
|
var keyFn = opts.keyFn || _clientIp;
|
|
405
409
|
var statusOnLimit = opts.statusOnLimit || 429;
|
|
406
410
|
var bodyOnLimit = opts.bodyOnLimit !== undefined ? opts.bodyOnLimit : "Too Many Requests";
|
|
411
|
+
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
412
|
+
var problemMode = opts.problemDetails === true;
|
|
407
413
|
var emitHeaders = opts.header !== false;
|
|
408
414
|
// headerPrefix (default "X-RateLimit-") builds the limit/remaining header
|
|
409
415
|
// names as <prefix>Limit / <prefix>Remaining. The X-RateLimit-* family is a
|
|
@@ -455,10 +461,21 @@ function create(opts) {
|
|
|
455
461
|
requestId: req.requestId,
|
|
456
462
|
});
|
|
457
463
|
} catch (_e) { /* audit best-effort */ }
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
464
|
+
var retryAfter = verdict.retryAfter > 0 ? verdict.retryAfter : null;
|
|
465
|
+
denyResponse(req, res, {
|
|
466
|
+
onDeny: onDeny,
|
|
467
|
+
problem: problemMode,
|
|
468
|
+
status: statusOnLimit,
|
|
469
|
+
info: { status: statusOnLimit, reason: "rate-limit-exceeded",
|
|
470
|
+
limit: verdict.limit, remaining: verdict.remaining,
|
|
471
|
+
retryAfter: verdict.retryAfter, key: k },
|
|
472
|
+
problemCode: "rate-limit-exceeded",
|
|
473
|
+
problemTitle: "Too Many Requests",
|
|
474
|
+
problemDetail: "Request rate limit exceeded; retry after the indicated interval.",
|
|
475
|
+
problemExt: retryAfter !== null ? { retryAfter: retryAfter } : null,
|
|
476
|
+
contentType: "text/plain",
|
|
477
|
+
body: bodyOnLimit,
|
|
478
|
+
});
|
|
462
479
|
}
|
|
463
480
|
|
|
464
481
|
var middleware = function rateLimit(req, res, next) {
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
var lazyRequire = require("../lazy-require");
|
|
22
22
|
var requestHelpers = require("../request-helpers");
|
|
23
23
|
var validateOpts = require("../validate-opts");
|
|
24
|
+
var denyResponse = require("./deny-response").denyResponse;
|
|
24
25
|
var { AuthError } = require("../framework-error");
|
|
25
26
|
|
|
26
27
|
var aal = lazyRequire(function () { return require("../auth/aal"); });
|
|
27
28
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
28
29
|
|
|
29
|
-
function _writeUnauthorized(res, requiredBand, actualBand, realm) {
|
|
30
|
+
function _writeUnauthorized(req, res, requiredBand, actualBand, realm, onDeny, problemMode) {
|
|
30
31
|
if (res.headersSent) return;
|
|
31
32
|
var body = JSON.stringify({
|
|
32
33
|
error: "step_up_required",
|
|
@@ -36,14 +37,24 @@ function _writeUnauthorized(res, requiredBand, actualBand, realm) {
|
|
|
36
37
|
});
|
|
37
38
|
var realmStr = realm ? ' realm="' + realm + '"' : "";
|
|
38
39
|
var challenge = "AAL-StepUp" + realmStr + ', required="' + requiredBand + '"';
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
denyResponse(req, res, {
|
|
41
|
+
onDeny: onDeny,
|
|
42
|
+
problem: problemMode,
|
|
43
|
+
status: 401, // HTTP 401 status
|
|
44
|
+
info: { status: 401, reason: "step_up_required",
|
|
45
|
+
required_aal: requiredBand, actual_aal: actualBand || null },
|
|
46
|
+
problemCode: "step-up-required",
|
|
47
|
+
problemTitle: "Step-Up Authentication Required",
|
|
48
|
+
problemDetail: "AAL " + requiredBand + " is required for this resource.",
|
|
49
|
+
problemExt: { required_aal: requiredBand, actual_aal: actualBand || null },
|
|
50
|
+
headers: {
|
|
51
|
+
"WWW-Authenticate": challenge,
|
|
52
|
+
// RFC 9111 §5.2.2.5 — auth-gated 401 must not be cached.
|
|
53
|
+
"Cache-Control": "no-store",
|
|
54
|
+
},
|
|
55
|
+
contentType: "application/json; charset=utf-8",
|
|
56
|
+
body: body,
|
|
45
57
|
});
|
|
46
|
-
res.end(body);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/**
|
|
@@ -68,6 +79,8 @@ function _writeUnauthorized(res, requiredBand, actualBand, realm) {
|
|
|
68
79
|
* getAal: function(req): string,
|
|
69
80
|
* realm: string,
|
|
70
81
|
* audit: boolean, // default true
|
|
82
|
+
* onDeny: function(req, res, info): void, // own the 401; info = { status, reason, required_aal, actual_aal }
|
|
83
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
|
|
71
84
|
* }
|
|
72
85
|
*
|
|
73
86
|
* @example
|
|
@@ -78,7 +91,7 @@ function _writeUnauthorized(res, requiredBand, actualBand, realm) {
|
|
|
78
91
|
function create(opts) {
|
|
79
92
|
opts = opts || {};
|
|
80
93
|
validateOpts(opts, [
|
|
81
|
-
"minimum", "getAal", "audit", "realm",
|
|
94
|
+
"minimum", "getAal", "audit", "realm", "onDeny", "problemDetails",
|
|
82
95
|
], "middleware.requireAal");
|
|
83
96
|
|
|
84
97
|
var minimum = opts.minimum;
|
|
@@ -92,6 +105,8 @@ function create(opts) {
|
|
|
92
105
|
|
|
93
106
|
var auditOn = opts.audit !== false;
|
|
94
107
|
var realm = (typeof opts.realm === "string" && opts.realm.length > 0) ? opts.realm : null;
|
|
108
|
+
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
109
|
+
var problemMode = opts.problemDetails === true;
|
|
95
110
|
|
|
96
111
|
return function requireAalMiddleware(req, res, next) {
|
|
97
112
|
var actual = null;
|
|
@@ -116,7 +131,7 @@ function create(opts) {
|
|
|
116
131
|
});
|
|
117
132
|
} catch (_ignored) { /* drop-silent */ }
|
|
118
133
|
}
|
|
119
|
-
return _writeUnauthorized(res, minimum, actual, realm);
|
|
134
|
+
return _writeUnauthorized(req, res, minimum, actual, realm, onDeny, problemMode);
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
if (auditOn) {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
var lazyRequire = require("../lazy-require");
|
|
40
40
|
var requestHelpers = require("../request-helpers");
|
|
41
41
|
var validateOpts = require("../validate-opts");
|
|
42
|
+
var denyResponse = require("./deny-response").denyResponse;
|
|
42
43
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
43
44
|
|
|
44
45
|
function _defaultPrefersJson(req) {
|
|
@@ -72,6 +73,8 @@ function _defaultPrefersJson(req) {
|
|
|
72
73
|
* prefersJson: function(req): boolean,
|
|
73
74
|
* errorMessage: string, // default "Authentication required."
|
|
74
75
|
* audit: boolean, // default true
|
|
76
|
+
* onDeny: function(req, res, info): void, // own any refusal shape; info = { status, reason, redirectTo }
|
|
77
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json for the 401 (redirect path unaffected)
|
|
75
78
|
* }
|
|
76
79
|
*
|
|
77
80
|
* @example
|
|
@@ -83,7 +86,7 @@ function _defaultPrefersJson(req) {
|
|
|
83
86
|
function create(opts) {
|
|
84
87
|
opts = opts || {};
|
|
85
88
|
validateOpts(opts, [
|
|
86
|
-
"redirectTo", "prefersJson", "errorMessage", "audit",
|
|
89
|
+
"redirectTo", "prefersJson", "errorMessage", "audit", "onDeny", "problemDetails",
|
|
87
90
|
], "middleware.requireAuth");
|
|
88
91
|
var redirectTo = opts.redirectTo || null;
|
|
89
92
|
var prefersJson = typeof opts.prefersJson === "function"
|
|
@@ -91,6 +94,8 @@ function create(opts) {
|
|
|
91
94
|
: _defaultPrefersJson;
|
|
92
95
|
var msg = opts.errorMessage || "Authentication required.";
|
|
93
96
|
var auditOn = opts.audit !== false;
|
|
97
|
+
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
98
|
+
var problemMode = opts.problemDetails === true;
|
|
94
99
|
|
|
95
100
|
return function requireAuth(req, res, next) {
|
|
96
101
|
if (req.user) return next();
|
|
@@ -107,6 +112,18 @@ function create(opts) {
|
|
|
107
112
|
} catch (_e) { /* audit best-effort */ }
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
// Operator hook owns ANY refusal shape (json / redirect / text)
|
|
116
|
+
// before the default content-negotiation runs.
|
|
117
|
+
if (onDeny) {
|
|
118
|
+
try {
|
|
119
|
+
var returned = onDeny(req, res, { status: 401, reason: "no-authenticated-user", redirectTo: redirectTo });
|
|
120
|
+
if (res.writableEnded) return returned;
|
|
121
|
+
} catch (_e) {
|
|
122
|
+
if (res.writableEnded) return;
|
|
123
|
+
// fall through to default
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
110
127
|
// RFC 9111 §5.2.2.5 — auth-gated paths SHOULD emit
|
|
111
128
|
// Cache-Control: no-store so a shared cache (or browser
|
|
112
129
|
// back-button cache) can't replay a 401 / redirect / payload
|
|
@@ -115,27 +132,26 @@ function create(opts) {
|
|
|
115
132
|
// cache directive, leaving the operator to set it themselves;
|
|
116
133
|
// forgetting it under a CDN that respects Cache-Control was
|
|
117
134
|
// a routine misconfiguration.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
122
|
-
res.end(JSON.stringify({ error: msg }));
|
|
123
|
-
}
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (redirectTo) {
|
|
127
|
-
if (typeof res.writeHead === "function") {
|
|
135
|
+
var wantsJson = prefersJson(req);
|
|
136
|
+
if (!wantsJson && redirectTo) {
|
|
137
|
+
if (!res.writableEnded && typeof res.writeHead === "function") {
|
|
128
138
|
// 302 Found — RFC 7231 §6.4.3. Not in HTTP_STATUS table.
|
|
129
139
|
res.writeHead(302, { "Location": redirectTo, "Cache-Control": "no-store" });
|
|
130
140
|
res.end();
|
|
131
141
|
}
|
|
132
142
|
return;
|
|
133
143
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
denyResponse(req, res, {
|
|
145
|
+
problem: problemMode,
|
|
146
|
+
status: requestHelpers.HTTP_STATUS.UNAUTHORIZED,
|
|
147
|
+
info: { status: 401, reason: "no-authenticated-user" },
|
|
148
|
+
problemCode: "authentication-required",
|
|
149
|
+
problemTitle: "Unauthorized",
|
|
150
|
+
problemDetail: msg,
|
|
151
|
+
headers: { "Cache-Control": "no-store" },
|
|
152
|
+
contentType: wantsJson ? "application/json" : "text/plain",
|
|
153
|
+
body: wantsJson ? JSON.stringify({ error: msg }) : msg,
|
|
154
|
+
});
|
|
139
155
|
};
|
|
140
156
|
}
|
|
141
157
|
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
var defineClass = require("../framework-error").defineClass;
|
|
52
52
|
var lazyRequire = require("../lazy-require");
|
|
53
53
|
var validateOpts = require("../validate-opts");
|
|
54
|
+
var denyResponse = require("./deny-response").denyResponse;
|
|
54
55
|
|
|
55
56
|
var bCrypto = lazyRequire(function () { return require("../crypto"); });
|
|
56
57
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
@@ -98,6 +99,8 @@ function _timingSafeStringEqual(a, b) {
|
|
|
98
99
|
* errorMessage: string,
|
|
99
100
|
* auditAction: string,
|
|
100
101
|
* audit: object,
|
|
102
|
+
* onDeny: function(req, res, info): void, // own the refusal; info = { status, reason, ...metadata }
|
|
103
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
|
|
101
104
|
* }
|
|
102
105
|
*
|
|
103
106
|
* @example
|
|
@@ -116,7 +119,7 @@ function create(opts) {
|
|
|
116
119
|
validateOpts(opts, [
|
|
117
120
|
"resolver", "requiredScopes", "getBoundField",
|
|
118
121
|
"audit", "auditAction", "errorMessage",
|
|
119
|
-
"tolerateMissingPeerCert",
|
|
122
|
+
"tolerateMissingPeerCert", "onDeny", "problemDetails",
|
|
120
123
|
], "middleware.requireBoundKey");
|
|
121
124
|
|
|
122
125
|
if (typeof opts.resolver !== "function") {
|
|
@@ -150,6 +153,8 @@ function create(opts) {
|
|
|
150
153
|
// even when peerCertFingerprints is set on the registered key.
|
|
151
154
|
// Production deployments leave this at default false.
|
|
152
155
|
var tolerateMissingPeerCert = !!opts.tolerateMissingPeerCert;
|
|
156
|
+
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
157
|
+
var problemMode = opts.problemDetails === true;
|
|
153
158
|
|
|
154
159
|
function _emitAudit(outcome, metadata) {
|
|
155
160
|
if (!auditOn) return;
|
|
@@ -162,30 +167,56 @@ function create(opts) {
|
|
|
162
167
|
} catch (_e) { /* drop-silent */ }
|
|
163
168
|
}
|
|
164
169
|
|
|
165
|
-
|
|
170
|
+
// RFC 6750 §3 — the Bearer challenge carries an error code that
|
|
171
|
+
// matches the failure: 401 with no token presented omits the code
|
|
172
|
+
// entirely; an unknown / revoked token is `invalid_token`; a 403
|
|
173
|
+
// missing-scope is `insufficient_scope`; a malformed bound-field
|
|
174
|
+
// request is `invalid_request`. Server-side failures (500 / 503)
|
|
175
|
+
// are not authentication challenges, so they advertise the scheme
|
|
176
|
+
// without an (incorrect) auth-error code.
|
|
177
|
+
function _bearerChallenge(status, reason) {
|
|
178
|
+
if (status === 401) {
|
|
179
|
+
if (reason === "no-bearer-token") return 'Bearer realm="api"';
|
|
180
|
+
return 'Bearer realm="api", error="invalid_token"';
|
|
181
|
+
}
|
|
182
|
+
if (status === 403) return 'Bearer realm="api", error="insufficient_scope"';
|
|
183
|
+
if (status === 400) return 'Bearer realm="api", error="invalid_request"'; // HTTP 400
|
|
184
|
+
return 'Bearer realm="api"';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _refuse(req, res, status, reason, metadata) {
|
|
166
188
|
_emitAudit("denied", Object.assign({ reason: reason }, metadata || {}));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
denyResponse(req, res, {
|
|
190
|
+
onDeny: onDeny,
|
|
191
|
+
problem: problemMode,
|
|
192
|
+
status: status,
|
|
193
|
+
info: Object.assign({ status: status, reason: reason }, metadata || {}),
|
|
194
|
+
problemCode: "bound-key-refused",
|
|
195
|
+
problemTitle: errorMessage,
|
|
196
|
+
problemDetail: "API key authentication failed: " + reason + ".",
|
|
197
|
+
problemExt: { reason: reason },
|
|
198
|
+
headers: {
|
|
199
|
+
"WWW-Authenticate": _bearerChallenge(status, reason),
|
|
200
|
+
"Cache-Control": "no-store",
|
|
201
|
+
},
|
|
202
|
+
contentType: "application/json; charset=utf-8",
|
|
203
|
+
body: JSON.stringify({ error: errorMessage, reason: reason }),
|
|
172
204
|
});
|
|
173
|
-
res.end(JSON.stringify({ error: errorMessage, reason: reason }));
|
|
174
205
|
}
|
|
175
206
|
|
|
176
207
|
return async function requireBoundKeyMiddleware(req, res, next) {
|
|
177
208
|
var apiKey = _parseBearer(req);
|
|
178
|
-
if (!apiKey) return _refuse(res, 401, "no-bearer-token", {});
|
|
209
|
+
if (!apiKey) return _refuse(req, res, 401, "no-bearer-token", {});
|
|
179
210
|
|
|
180
211
|
var record;
|
|
181
212
|
try { record = await resolver(apiKey); }
|
|
182
213
|
catch (e) {
|
|
183
|
-
return _refuse(res, 503, "resolver-unavailable", {
|
|
214
|
+
return _refuse(req, res, 503, "resolver-unavailable", {
|
|
184
215
|
error: (e && e.message) || String(e),
|
|
185
216
|
});
|
|
186
217
|
}
|
|
187
218
|
if (!record || typeof record !== "object") {
|
|
188
|
-
return _refuse(res, 401, "key-unknown-or-revoked", {});
|
|
219
|
+
return _refuse(req, res, 401, "key-unknown-or-revoked", {});
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
// Required-scope check — operator-supplied requiredScopes must be
|
|
@@ -193,7 +224,7 @@ function create(opts) {
|
|
|
193
224
|
var keyScopes = Array.isArray(record.scopes) ? record.scopes : [];
|
|
194
225
|
for (var rsi = 0; rsi < requiredScopes.length; rsi++) {
|
|
195
226
|
if (keyScopes.indexOf(requiredScopes[rsi]) === -1) {
|
|
196
|
-
return _refuse(res, 403, "missing-scope", {
|
|
227
|
+
return _refuse(req, res, 403, "missing-scope", {
|
|
197
228
|
requiredScope: requiredScopes[rsi], keyId: record.id || null,
|
|
198
229
|
});
|
|
199
230
|
}
|
|
@@ -208,25 +239,25 @@ function create(opts) {
|
|
|
208
239
|
var fieldName = registeredKeys[bfi];
|
|
209
240
|
var getter = getBoundField[fieldName];
|
|
210
241
|
if (!getter) {
|
|
211
|
-
return _refuse(res, 500, "bound-field-no-getter", {
|
|
242
|
+
return _refuse(req, res, 500, "bound-field-no-getter", {
|
|
212
243
|
field: fieldName, keyId: record.id || null,
|
|
213
244
|
});
|
|
214
245
|
}
|
|
215
246
|
var presented;
|
|
216
247
|
try { presented = getter(req); }
|
|
217
248
|
catch (e) {
|
|
218
|
-
return _refuse(res, 400, "bound-field-getter-threw", { // HTTP 400
|
|
249
|
+
return _refuse(req, res, 400, "bound-field-getter-threw", { // HTTP 400
|
|
219
250
|
field: fieldName, error: (e && e.message) || String(e),
|
|
220
251
|
});
|
|
221
252
|
}
|
|
222
253
|
if (typeof presented !== "string" || presented.length === 0) {
|
|
223
|
-
return _refuse(res, 400, "bound-field-missing", { // HTTP 400
|
|
254
|
+
return _refuse(req, res, 400, "bound-field-missing", { // HTTP 400
|
|
224
255
|
field: fieldName, keyId: record.id || null,
|
|
225
256
|
});
|
|
226
257
|
}
|
|
227
258
|
var expected = String(registered[fieldName]);
|
|
228
259
|
if (!_timingSafeStringEqual(presented, expected)) {
|
|
229
|
-
return _refuse(res, 403, "bound-field-mismatch", {
|
|
260
|
+
return _refuse(req, res, 403, "bound-field-mismatch", {
|
|
230
261
|
field: fieldName, keyId: record.id || null,
|
|
231
262
|
});
|
|
232
263
|
}
|
|
@@ -252,7 +283,7 @@ function create(opts) {
|
|
|
252
283
|
// Audited bypass for dev fixtures.
|
|
253
284
|
_emitAudit("denied", { reason: "peer-cert-bypass-tolerated", keyId: record.id });
|
|
254
285
|
} else {
|
|
255
|
-
return _refuse(res, 401, "peer-cert-required", {
|
|
286
|
+
return _refuse(req, res, 401, "peer-cert-required", {
|
|
256
287
|
keyId: record.id || null,
|
|
257
288
|
});
|
|
258
289
|
}
|
|
@@ -262,7 +293,7 @@ function create(opts) {
|
|
|
262
293
|
// because it does the same constant-time hex/colon comparison
|
|
263
294
|
// we want for an allow-list. A future refactor can rename to
|
|
264
295
|
// isCertFingerprintInSet — semantically identical.
|
|
265
|
-
return _refuse(res, 403, "peer-cert-not-pinned", {
|
|
296
|
+
return _refuse(req, res, 403, "peer-cert-not-pinned", {
|
|
266
297
|
fingerprint: fpColon, keyId: record.id || null,
|
|
267
298
|
});
|
|
268
299
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
var lazyRequire = require("../lazy-require");
|
|
21
|
+
var denyResponse = require("./deny-response").denyResponse;
|
|
21
22
|
var { defineClass } = require("../framework-error");
|
|
22
23
|
|
|
23
24
|
var RequireContentTypeError = defineClass("RequireContentTypeError", { alwaysPermanent: true });
|
|
@@ -58,8 +59,10 @@ function _normalizeAllowed(types) {
|
|
|
58
59
|
*
|
|
59
60
|
* @opts
|
|
60
61
|
* {
|
|
61
|
-
* methods:
|
|
62
|
-
* audit:
|
|
62
|
+
* methods: string[], // override default ["POST", "PUT", "PATCH"]
|
|
63
|
+
* audit: boolean, // default true
|
|
64
|
+
* onDeny: function(req, res, info): void, // own the 415; info = { status, reason, contentType, accepted }
|
|
65
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
|
|
63
66
|
* }
|
|
64
67
|
*
|
|
65
68
|
* @example
|
|
@@ -82,6 +85,8 @@ function create(allowed, opts) {
|
|
|
82
85
|
? opts.methods.map(function (m) { return m.toUpperCase(); })
|
|
83
86
|
: DEFAULT_BODY_METHODS.slice();
|
|
84
87
|
var auditOn = opts.audit !== false;
|
|
88
|
+
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
89
|
+
var problemMode = opts.problemDetails === true;
|
|
85
90
|
|
|
86
91
|
return function requireContentTypeMiddleware(req, res, next) {
|
|
87
92
|
var m = (req.method || "").toUpperCase();
|
|
@@ -90,13 +95,19 @@ function create(allowed, opts) {
|
|
|
90
95
|
var bare = (typeof ct === "string" ? ct.split(";")[0].trim().toLowerCase() : "");
|
|
91
96
|
if (bare.length > 0 && normalized.indexOf(bare) !== -1) return next();
|
|
92
97
|
if (!res.headersSent) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
+
denyResponse(req, res, {
|
|
99
|
+
onDeny: onDeny,
|
|
100
|
+
problem: problemMode,
|
|
101
|
+
status: 415,
|
|
102
|
+
info: { status: 415, reason: "unsupported-media-type",
|
|
103
|
+
contentType: bare || null, accepted: normalized },
|
|
104
|
+
problemCode: "unsupported-media-type",
|
|
105
|
+
problemTitle: "Unsupported Media Type",
|
|
106
|
+
problemDetail: "The request Content-Type is not accepted on this resource.",
|
|
107
|
+
headers: { "Accept": normalized.join(", ") },
|
|
108
|
+
contentType: "text/plain; charset=utf-8",
|
|
109
|
+
body: "Unsupported Media Type",
|
|
98
110
|
});
|
|
99
|
-
res.end(body);
|
|
100
111
|
}
|
|
101
112
|
if (auditOn) {
|
|
102
113
|
try {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
var lazyRequire = require("../lazy-require");
|
|
19
|
+
var denyResponse = require("./deny-response").denyResponse;
|
|
19
20
|
var { defineClass } = require("../framework-error");
|
|
20
21
|
|
|
21
22
|
var RequireMethodsError = defineClass("RequireMethodsError", { alwaysPermanent: true });
|
|
@@ -38,7 +39,9 @@ var observability = lazyRequire(function () { return require("../observability")
|
|
|
38
39
|
*
|
|
39
40
|
* @opts
|
|
40
41
|
* {
|
|
41
|
-
* audit:
|
|
42
|
+
* audit: boolean, // default true
|
|
43
|
+
* onDeny: function(req, res, info): void, // own the 405; info = { status, reason, method, allowed }
|
|
44
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of text/plain
|
|
42
45
|
* }
|
|
43
46
|
*
|
|
44
47
|
* @example
|
|
@@ -69,18 +72,25 @@ function create(allowed, opts) {
|
|
|
69
72
|
var allowHeader = normalized.join(", ");
|
|
70
73
|
opts = opts || {};
|
|
71
74
|
var auditOn = opts.audit !== false;
|
|
75
|
+
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
76
|
+
var problemMode = opts.problemDetails === true;
|
|
72
77
|
|
|
73
78
|
return function requireMethodsMiddleware(req, res, next) {
|
|
74
79
|
var m = (req.method || "").toUpperCase();
|
|
75
80
|
if (normalized.indexOf(m) !== -1) return next();
|
|
76
81
|
if (!res.headersSent) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
+
denyResponse(req, res, {
|
|
83
|
+
onDeny: onDeny,
|
|
84
|
+
problem: problemMode,
|
|
85
|
+
status: 405,
|
|
86
|
+
info: { status: 405, reason: "method-not-allowed", method: m, allowed: normalized },
|
|
87
|
+
problemCode: "method-not-allowed",
|
|
88
|
+
problemTitle: "Method Not Allowed",
|
|
89
|
+
problemDetail: "The " + m + " method is not allowed on this resource.",
|
|
90
|
+
headers: { "Allow": allowHeader },
|
|
91
|
+
contentType: "text/plain; charset=utf-8",
|
|
92
|
+
body: "Method Not Allowed",
|
|
82
93
|
});
|
|
83
|
-
res.end(body);
|
|
84
94
|
}
|
|
85
95
|
if (auditOn) {
|
|
86
96
|
try {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
var defineClass = require("../framework-error").defineClass;
|
|
49
49
|
var lazyRequire = require("../lazy-require");
|
|
50
50
|
var validateOpts = require("../validate-opts");
|
|
51
|
+
var denyResponse = require("./deny-response").denyResponse;
|
|
51
52
|
|
|
52
53
|
var bCrypto = lazyRequire(function () { return require("../crypto"); });
|
|
53
54
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
@@ -84,6 +85,8 @@ function _normalizeFingerprintEntry(entry) {
|
|
|
84
85
|
* fingerprintAllowList: string[],
|
|
85
86
|
* denyList: string[],
|
|
86
87
|
* onAuthenticated: function(req, res, next): void,
|
|
88
|
+
* onDeny: function(req, res, info): void, // own the refusal (mirrors onAuthenticated); info = { status, reason, ...metadata }
|
|
89
|
+
* problemDetails: boolean, // default false — emit RFC 9457 application/problem+json instead of the default JSON envelope
|
|
87
90
|
* auditAction: string,
|
|
88
91
|
* errorMessage: string,
|
|
89
92
|
* audit: object,
|
|
@@ -100,7 +103,7 @@ function create(opts) {
|
|
|
100
103
|
opts = opts || {};
|
|
101
104
|
validateOpts(opts, [
|
|
102
105
|
"fingerprintAllowList", "denyList",
|
|
103
|
-
"onAuthenticated", "audit",
|
|
106
|
+
"onAuthenticated", "onDeny", "problemDetails", "audit",
|
|
104
107
|
"auditAction", "errorMessage",
|
|
105
108
|
], "middleware.requireMtls");
|
|
106
109
|
|
|
@@ -109,6 +112,8 @@ function create(opts) {
|
|
|
109
112
|
var denyList = Array.isArray(opts.denyList)
|
|
110
113
|
? opts.denyList.map(_normalizeFingerprintEntry) : [];
|
|
111
114
|
var onAuthenticated = typeof opts.onAuthenticated === "function" ? opts.onAuthenticated : null;
|
|
115
|
+
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
116
|
+
var problemMode = opts.problemDetails === true;
|
|
112
117
|
var auditOn = opts.audit !== false;
|
|
113
118
|
var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
|
|
114
119
|
? opts.auditAction : "mtls.required";
|
|
@@ -126,16 +131,24 @@ function create(opts) {
|
|
|
126
131
|
} catch (_e) { /* drop-silent — audit is best-effort, never blocks the request */ }
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
function _refuse(res, reason, metadata) {
|
|
134
|
+
function _refuse(req, res, reason, metadata) {
|
|
130
135
|
_emit("denied", Object.assign({ reason: reason }, metadata || {}));
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
denyResponse(req, res, {
|
|
137
|
+
onDeny: onDeny,
|
|
138
|
+
problem: problemMode,
|
|
139
|
+
status: 401,
|
|
140
|
+
info: Object.assign({ status: 401, reason: reason }, metadata || {}),
|
|
141
|
+
problemCode: "client-certificate-required",
|
|
142
|
+
problemTitle: "Unauthorized",
|
|
143
|
+
problemDetail: errorMessage,
|
|
144
|
+
problemExt: { reason: reason },
|
|
145
|
+
headers: {
|
|
134
146
|
"WWW-Authenticate": "Mutual",
|
|
135
147
|
"Cache-Control": "no-store",
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
},
|
|
149
|
+
contentType: "application/json; charset=utf-8",
|
|
150
|
+
body: JSON.stringify({ error: errorMessage, reason: reason }),
|
|
151
|
+
});
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
return function requireMtlsMiddleware(req, res, next) {
|
|
@@ -158,10 +171,10 @@ function create(opts) {
|
|
|
158
171
|
|
|
159
172
|
if (!authorized) {
|
|
160
173
|
var authzError = (sock && sock.authorizationError) || "no-peer-cert";
|
|
161
|
-
return _refuse(res, "tls-unauthorized", { authorizationError: String(authzError) });
|
|
174
|
+
return _refuse(req, res, "tls-unauthorized", { authorizationError: String(authzError) });
|
|
162
175
|
}
|
|
163
176
|
if (!peerCert || !peerCert.raw) {
|
|
164
|
-
return _refuse(res, "no-peer-cert", {});
|
|
177
|
+
return _refuse(req, res, "no-peer-cert", {});
|
|
165
178
|
}
|
|
166
179
|
|
|
167
180
|
// Compute fingerprint via the framework's SHA3-512 helper. Buffer
|
|
@@ -171,17 +184,17 @@ function create(opts) {
|
|
|
171
184
|
try {
|
|
172
185
|
fp = bCrypto().hashCertFingerprint(peerCert.raw);
|
|
173
186
|
} catch (e) {
|
|
174
|
-
return _refuse(res, "fingerprint-failed", { error: (e && e.message) || String(e) });
|
|
187
|
+
return _refuse(req, res, "fingerprint-failed", { error: (e && e.message) || String(e) });
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
if (denyList.length > 0 && bCrypto().isCertRevoked(peerCert.raw, denyList)) {
|
|
178
|
-
return _refuse(res, "fingerprint-on-deny-list", {
|
|
191
|
+
return _refuse(req, res, "fingerprint-on-deny-list", {
|
|
179
192
|
fingerprint: fp.colon,
|
|
180
193
|
subject: (peerCert.subject && peerCert.subject.CN) || null,
|
|
181
194
|
});
|
|
182
195
|
}
|
|
183
196
|
if (allowList && allowList.length > 0 && !bCrypto().isCertRevoked(peerCert.raw, allowList)) {
|
|
184
|
-
return _refuse(res, "fingerprint-not-allowed", {
|
|
197
|
+
return _refuse(req, res, "fingerprint-not-allowed", {
|
|
185
198
|
fingerprint: fp.colon,
|
|
186
199
|
subject: (peerCert.subject && peerCert.subject.CN) || null,
|
|
187
200
|
});
|
|
@@ -199,7 +212,7 @@ function create(opts) {
|
|
|
199
212
|
if (onAuthenticated) {
|
|
200
213
|
try { return onAuthenticated(req, res, next); }
|
|
201
214
|
catch (e) {
|
|
202
|
-
return _refuse(res, "on-authenticated-threw", { error: (e && e.message) || String(e) });
|
|
215
|
+
return _refuse(req, res, "on-authenticated-threw", { error: (e && e.message) || String(e) });
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
218
|
return next();
|
|
@@ -126,7 +126,7 @@ var DEFAULT_MAX_TTL_MS = C.TIME.hours(24);
|
|
|
126
126
|
var DEFAULT_MIN_TTL_MS = C.TIME.seconds(60);
|
|
127
127
|
var DEFAULT_STALE_WINDOW = C.TIME.hours(6);
|
|
128
128
|
var DEFAULT_PROFILE = "strict";
|
|
129
|
-
//
|
|
129
|
+
// CWE-400/770. Bound the cache so a hostile peer
|
|
130
130
|
// that can drive query-name selection (e.g. inbound SMTP forwarding
|
|
131
131
|
// DKIM `s=` / `d=` tag-controlled lookups) cannot inflate the Map to
|
|
132
132
|
// OOM. Default 5000 entries: a parsed-response object ~100 bytes ×
|
|
@@ -216,7 +216,7 @@ function create(opts) {
|
|
|
216
216
|
|
|
217
217
|
var cache = new Map(); // key → { response, parsed, ttl, expiresAt, staleUntil }
|
|
218
218
|
|
|
219
|
-
// CWE-400/770
|
|
219
|
+
// CWE-400/770. LRU eviction on insert when the cache is at
|
|
220
220
|
// capacity. v8 Map preserves insertion order; oldest key is the
|
|
221
221
|
// first entry returned by Map.keys().next().
|
|
222
222
|
function _evictIfFull() {
|
package/lib/network-dns.js
CHANGED
|
@@ -39,8 +39,7 @@ var STATE = {
|
|
|
39
39
|
// Default-on secure DNS (DoH via Cloudflare) when neither doh nor dot
|
|
40
40
|
// is operator-configured AND no opt-out env var is set. Operators
|
|
41
41
|
// who explicitly want the system resolver call useSystemResolver()
|
|
42
|
-
// or set BLAMEJS_DNS_TRANSPORT=system. Default-on
|
|
43
|
-
// ("security defaults are not opt-in").
|
|
42
|
+
// or set BLAMEJS_DNS_TRANSPORT=system. Default-on (security defaults are not opt-in).
|
|
44
43
|
systemResolver: false,
|
|
45
44
|
};
|
|
46
45
|
|
package/lib/network-tls.js
CHANGED
|
@@ -1136,7 +1136,6 @@ function evaluateOcspResponse(ocspDer, opts) {
|
|
|
1136
1136
|
// length inputs but fast-paths on length mismatch; not security-
|
|
1137
1137
|
// critical here (the OCSP response is CA-signed and signature
|
|
1138
1138
|
// already verified) but matches the project discipline.
|
|
1139
|
-
// (Audit 2026-05-11.)
|
|
1140
1139
|
if (!bCrypto.timingSafeEqual(parsed.basic.nonce, opts.expectedNonce)) {
|
|
1141
1140
|
return { ok: false, status: parsed.status, signatureValid: true,
|
|
1142
1141
|
errors: ["OCSP nonce mismatch — possible replay or wrong responder"] };
|