@blamejs/core 0.6.13 → 0.6.21
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 +8 -0
- package/NOTICE +16 -0
- package/README.md +27 -18
- package/index.js +12 -0
- package/lib/archive.js +8 -7
- package/lib/audit.js +4 -0
- package/lib/auth/password.js +449 -4
- package/lib/bundler.js +8 -8
- package/lib/cache.js +105 -20
- package/lib/cli.js +598 -4
- package/lib/config-drift.js +309 -0
- package/lib/crypto-field.js +37 -0
- package/lib/crypto.js +8 -0
- package/lib/db-query.js +21 -2
- package/lib/db.js +32 -2
- package/lib/dual-control.js +475 -0
- package/lib/file-type.js +265 -0
- package/lib/framework-schema.js +38 -6
- package/lib/http-client-cookie-jar.js +117 -17
- package/lib/http-client.js +81 -3
- package/lib/internal-sha1-hibp.js +34 -0
- package/lib/mail.js +5 -4
- package/lib/middleware/csp-nonce.js +7 -4
- package/lib/middleware/index.js +2 -0
- package/lib/middleware/network-allowlist.js +199 -0
- package/lib/network-dns.js +564 -0
- package/lib/network-heartbeat.js +290 -0
- package/lib/network-nts.js +552 -0
- package/lib/network-proxy.js +246 -0
- package/lib/network-tls.js +326 -0
- package/lib/network.js +233 -0
- package/lib/ntp-check.js +50 -4
- package/lib/object-store/azure-blob.js +16 -42
- package/lib/pagination.js +136 -76
- package/lib/parsers/index.js +16 -2
- package/lib/parsers/safe-ini.js +273 -0
- package/lib/permissions.js +223 -9
- package/lib/pqc-agent.js +4 -4
- package/lib/retention.js +439 -0
- package/lib/security-assert.js +368 -0
- package/lib/session.js +138 -8
- package/lib/ssrf-guard.js +9 -0
- package/lib/vault/index.js +3 -3
- package/lib/vendor/MANIFEST.json +12 -0
- package/lib/vendor/common-passwords-top-10000.txt +10000 -0
- package/package.json +3 -2
- package/sbom.cyclonedx.json +61 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* internal-sha1-hibp — SHA-1 hex digest, RESTRICTED to the
|
|
4
|
+
* HaveIBeenPwned k-anonymity API caller (lib/auth/password.js policy).
|
|
5
|
+
*
|
|
6
|
+
* SHA-1 is broken for collision-resistance and trivially extendable;
|
|
7
|
+
* the framework MUST NOT use it for storage, signing, message
|
|
8
|
+
* authentication, key derivation, fingerprinting, or any other
|
|
9
|
+
* security-relevant path. The HIBP API mandates SHA-1 for backwards
|
|
10
|
+
* compatibility with leaked-password corpora; that's the only reason
|
|
11
|
+
* this exists.
|
|
12
|
+
*
|
|
13
|
+
* Why not export from b.crypto:
|
|
14
|
+
* - Public exports invite cargo-cult use. A future contributor
|
|
15
|
+
* would search `b.crypto.sha1*` and slot it into "I just need
|
|
16
|
+
* a quick hash" code — re-introducing SHA-1 into a crypto-
|
|
17
|
+
* relevant path the framework spent every other primitive
|
|
18
|
+
* keeping out.
|
|
19
|
+
* - The single legitimate caller (auth/password.js) requires it
|
|
20
|
+
* for HIBP interop only. Restricting it to that module via a
|
|
21
|
+
* filename + comment-block gate keeps the surface honest.
|
|
22
|
+
*
|
|
23
|
+
* If a SECOND legitimate use case for SHA-1 ever emerges (operator
|
|
24
|
+
* needs to interop with another SHA-1-mandated API), this module
|
|
25
|
+
* stays internal — the new caller imports it directly. Public
|
|
26
|
+
* `b.crypto.sha1*` is permanently off the table.
|
|
27
|
+
*/
|
|
28
|
+
var nodeCrypto = require("node:crypto");
|
|
29
|
+
|
|
30
|
+
function sha1Hex(data) {
|
|
31
|
+
return nodeCrypto.createHash("sha1").update(data).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { sha1Hex: sha1Hex };
|
package/lib/mail.js
CHANGED
|
@@ -403,10 +403,11 @@ function _buildRfc822(message) {
|
|
|
403
403
|
body = inner.body;
|
|
404
404
|
} else {
|
|
405
405
|
// multipart/mixed: first part is the body (single or alternative),
|
|
406
|
-
// subsequent parts are the attachments. Inline disposition +
|
|
407
|
-
// is interpreted correctly by every major client even
|
|
408
|
-
//
|
|
409
|
-
//
|
|
406
|
+
// subsequent parts are the attachments. Inline disposition +
|
|
407
|
+
// Content-ID is interpreted correctly by every major client even
|
|
408
|
+
// inside mixed. Operators needing strict-RFC-2387 multipart/related
|
|
409
|
+
// wrap the body via the mail.transports interface and pass a
|
|
410
|
+
// content-type override.
|
|
410
411
|
var mixedBoundary = _newBoundary("mixed");
|
|
411
412
|
headers.push('Content-Type: multipart/mixed; boundary="' + mixedBoundary + '"');
|
|
412
413
|
var parts = [];
|
|
@@ -248,10 +248,13 @@ function create(opts) {
|
|
|
248
248
|
var property = (typeof opts.property === "string" && opts.property.length > 0) ? opts.property : "cspNonce";
|
|
249
249
|
var always = !!opts.always;
|
|
250
250
|
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
251
|
+
// Token string used by the cacheable-render pattern: templates render
|
|
252
|
+
// with `cspNonce: nonceMw.PLACEHOLDER`, the rendered HTML is cached,
|
|
253
|
+
// then nonceMw.substitute(html, req) swaps in the per-request nonce
|
|
254
|
+
// at serve time. Per-instance random by default — multiple cspNonce
|
|
255
|
+
// instances in the same process get different placeholders, and the
|
|
256
|
+
// literal string never appears in source so operator content can't
|
|
257
|
+
// accidentally collide.
|
|
255
258
|
//
|
|
256
259
|
// Operators with caches that persist across process restarts (Redis,
|
|
257
260
|
// cluster backend) pass `opts.placeholder` to pin a stable token —
|
package/lib/middleware/index.js
CHANGED
|
@@ -34,6 +34,7 @@ module.exports = {
|
|
|
34
34
|
requestLog: require("./request-log").create,
|
|
35
35
|
apiEncrypt: require("./api-encrypt"),
|
|
36
36
|
dbRoleFor: require("./db-role-for").create,
|
|
37
|
+
networkAllowlist: require("./network-allowlist").create,
|
|
37
38
|
|
|
38
39
|
// Module exports for advanced use (constants, raw factory access)
|
|
39
40
|
_modules: {
|
|
@@ -54,5 +55,6 @@ module.exports = {
|
|
|
54
55
|
requestLog: require("./request-log"),
|
|
55
56
|
apiEncrypt: require("./api-encrypt"),
|
|
56
57
|
dbRoleFor: require("./db-role-for"),
|
|
58
|
+
networkAllowlist: require("./network-allowlist"),
|
|
57
59
|
},
|
|
58
60
|
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* network-allowlist — restrict an opt-named set of paths to operator-
|
|
4
|
+
* approved CIDR ranges.
|
|
5
|
+
*
|
|
6
|
+
* Path-based admin gates (perms.require, requireAuth) prevent UNAUTHORIZED
|
|
7
|
+
* users from reaching sensitive routes. They do NOT prevent the route
|
|
8
|
+
* from being REACHABLE from the public internet — a credential leak +
|
|
9
|
+
* a path-only gate is full compromise. Operators with a clear admin/
|
|
10
|
+
* non-admin network split want a network-layer fence on top of the
|
|
11
|
+
* application-layer gate.
|
|
12
|
+
*
|
|
13
|
+
* The cleanest place for that fence is the reverse proxy / NACL. This
|
|
14
|
+
* middleware is the in-process equivalent for operators who don't have
|
|
15
|
+
* separate infrastructure for it (small deploys, single-process apps,
|
|
16
|
+
* the wiki example default).
|
|
17
|
+
*
|
|
18
|
+
* var fence = b.middleware.networkAllowlist({
|
|
19
|
+
* paths: ["/admin", "/admin/", "/healthz/internal"],
|
|
20
|
+
* allowedCidrs: ["10.0.0.0/8", "192.168.0.0/16", "::1/128"],
|
|
21
|
+
* trustProxy: true, // honour x-forwarded-for; default false
|
|
22
|
+
* denyStatus: 404, // default — reveal nothing about the gate
|
|
23
|
+
* denyBody: "Not Found", // default
|
|
24
|
+
* audit: b.audit, // default: null — emits network.gate.denied
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* router.use(fence);
|
|
28
|
+
*
|
|
29
|
+
* Behaviour:
|
|
30
|
+
* - The middleware is path-scoped: requests whose pathname doesn't
|
|
31
|
+
* start with any of `paths` pass through unchanged. Hot-path-cheap.
|
|
32
|
+
* - Requests on a gated path get their client IP resolved through
|
|
33
|
+
* b.requestHelpers.clientIp(req, { trustProxy }) — same trust
|
|
34
|
+
* model as every other middleware that reads client IP.
|
|
35
|
+
* - The IP is checked against the CIDR allowlist using
|
|
36
|
+
* b.ssrfGuard.cidrContains. A miss returns denyStatus + denyBody
|
|
37
|
+
* and audits the rejection. Default 404 hides the gate's
|
|
38
|
+
* existence from probes.
|
|
39
|
+
*
|
|
40
|
+
* Validation policy:
|
|
41
|
+
* - opts: throw at create() time on bad shape (not-array paths /
|
|
42
|
+
* allowedCidrs, non-CIDR strings, denyStatus outside 4xx/5xx).
|
|
43
|
+
* - Per-request: a request that looks malformed (no socket, no
|
|
44
|
+
* headers) gets denied — fail closed.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
var lazyRequire = require("../lazy-require");
|
|
48
|
+
var requestHelpers = require("../request-helpers");
|
|
49
|
+
var ssrfGuard = require("../ssrf-guard");
|
|
50
|
+
var validateOpts = require("../validate-opts");
|
|
51
|
+
var { defineClass } = require("../framework-error");
|
|
52
|
+
|
|
53
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
54
|
+
|
|
55
|
+
var NetworkAllowlistError = defineClass("NetworkAllowlistError", { alwaysPermanent: true });
|
|
56
|
+
var _err = NetworkAllowlistError.factory;
|
|
57
|
+
|
|
58
|
+
function _validateCidr(cidr) {
|
|
59
|
+
// ssrfGuard.cidrContains tolerates any string at runtime (returns
|
|
60
|
+
// false on garbage), but the operator means business at config time
|
|
61
|
+
// — a typo'd CIDR silently disables that allow entry. Verify the
|
|
62
|
+
// shape now: <ipv4-or-ipv6>/<prefix-length>.
|
|
63
|
+
if (typeof cidr !== "string" || cidr.length === 0) return false;
|
|
64
|
+
var slash = cidr.indexOf("/");
|
|
65
|
+
if (slash < 1 || slash >= cidr.length - 1) return false;
|
|
66
|
+
var prefix = parseInt(cidr.slice(slash + 1), 10);
|
|
67
|
+
if (!isFinite(prefix) || prefix < 0) return false;
|
|
68
|
+
// Smoke-test the implementation against a known value — if it
|
|
69
|
+
// throws, the cidr is malformed.
|
|
70
|
+
try { ssrfGuard.cidrContains(cidr, "127.0.0.1"); return true; }
|
|
71
|
+
catch (_e) { return false; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function create(opts) {
|
|
75
|
+
opts = opts || {};
|
|
76
|
+
validateOpts(opts, [
|
|
77
|
+
"paths", "allowedCidrs", "deniedCidrs", "trustProxy",
|
|
78
|
+
"denyStatus", "denyBody", "audit",
|
|
79
|
+
], "middleware.networkAllowlist");
|
|
80
|
+
|
|
81
|
+
if (!Array.isArray(opts.paths) || opts.paths.length === 0) {
|
|
82
|
+
throw _err("BAD_OPT", "paths must be a non-empty array of pathname prefixes");
|
|
83
|
+
}
|
|
84
|
+
for (var pi = 0; pi < opts.paths.length; pi++) {
|
|
85
|
+
if (typeof opts.paths[pi] !== "string" || opts.paths[pi].charAt(0) !== "/") {
|
|
86
|
+
throw _err("BAD_OPT",
|
|
87
|
+
"paths[" + pi + "] must be a string starting with '/', got " + JSON.stringify(opts.paths[pi]));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(opts.allowedCidrs) || opts.allowedCidrs.length === 0) {
|
|
91
|
+
throw _err("BAD_OPT", "allowedCidrs must be a non-empty array of CIDR strings");
|
|
92
|
+
}
|
|
93
|
+
for (var ci = 0; ci < opts.allowedCidrs.length; ci++) {
|
|
94
|
+
if (!_validateCidr(opts.allowedCidrs[ci])) {
|
|
95
|
+
throw _err("BAD_OPT",
|
|
96
|
+
"allowedCidrs[" + ci + "] is not a valid CIDR, got " + JSON.stringify(opts.allowedCidrs[ci]));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// deniedCidrs takes precedence over allowedCidrs (deny-then-allow)
|
|
100
|
+
// — useful when an operator wants "10.0.0.0/8 except 10.0.99.0/24".
|
|
101
|
+
// Default empty (no deny rules).
|
|
102
|
+
var deniedCidrs = Array.isArray(opts.deniedCidrs) ? opts.deniedCidrs.slice() : [];
|
|
103
|
+
for (var di = 0; di < deniedCidrs.length; di++) {
|
|
104
|
+
if (!_validateCidr(deniedCidrs[di])) {
|
|
105
|
+
throw _err("BAD_OPT",
|
|
106
|
+
"deniedCidrs[" + di + "] is not a valid CIDR, got " + JSON.stringify(deniedCidrs[di]));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var paths = opts.paths.slice();
|
|
111
|
+
var allowedCidrs = opts.allowedCidrs.slice();
|
|
112
|
+
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
113
|
+
? opts.trustProxy : false;
|
|
114
|
+
var denyStatus = typeof opts.denyStatus === "number" ? opts.denyStatus : 404;
|
|
115
|
+
if (denyStatus < 400 || denyStatus >= 600 || Math.floor(denyStatus) !== denyStatus) {
|
|
116
|
+
throw _err("BAD_OPT", "denyStatus must be a 4xx or 5xx integer, got " + denyStatus);
|
|
117
|
+
}
|
|
118
|
+
var denyBody = typeof opts.denyBody === "string" ? opts.denyBody : "Not Found";
|
|
119
|
+
var auditOn = opts.audit !== false && opts.audit != null;
|
|
120
|
+
var auditInstance = opts.audit === true ? null : opts.audit; // null → use lazy-required default
|
|
121
|
+
|
|
122
|
+
function _emitDeny(req, ip, route) {
|
|
123
|
+
if (!auditOn) return;
|
|
124
|
+
var sink = auditInstance || audit();
|
|
125
|
+
try {
|
|
126
|
+
sink.safeEmit({
|
|
127
|
+
action: "network.gate.denied",
|
|
128
|
+
outcome: "denied",
|
|
129
|
+
actor: requestHelpers.extractActorContext(req),
|
|
130
|
+
resource: { kind: "http.path", id: route },
|
|
131
|
+
reason: "ip-not-in-allowlist",
|
|
132
|
+
metadata: { clientIp: ip, allowedCidrs: allowedCidrs },
|
|
133
|
+
});
|
|
134
|
+
} catch (_e) { /* audit best-effort */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _matchesPath(pathname) {
|
|
138
|
+
for (var i = 0; i < paths.length; i++) {
|
|
139
|
+
var prefix = paths[i];
|
|
140
|
+
if (pathname === prefix) return true;
|
|
141
|
+
// Boundary-aware: "/admin" matches "/admin" + "/admin/..." but
|
|
142
|
+
// not "/administer" — prevents accidental shadowing of
|
|
143
|
+
// similarly-named public routes.
|
|
144
|
+
if (pathname.length > prefix.length &&
|
|
145
|
+
pathname.indexOf(prefix) === 0 &&
|
|
146
|
+
(prefix.charAt(prefix.length - 1) === "/" || pathname.charAt(prefix.length) === "/")) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return function networkAllowlist(req, res, next) {
|
|
154
|
+
var pathname = req.pathname || (req.url || "").split("?")[0];
|
|
155
|
+
if (!_matchesPath(pathname)) return next();
|
|
156
|
+
|
|
157
|
+
var ip = requestHelpers.clientIp(req, { trustProxy: trustProxy });
|
|
158
|
+
if (!ip) {
|
|
159
|
+
// Fail closed: a request we can't even derive an IP for shouldn't
|
|
160
|
+
// bypass the gate.
|
|
161
|
+
_emitDeny(req, "<unknown>", pathname);
|
|
162
|
+
res.writeHead(denyStatus, { "Content-Type": "text/plain" });
|
|
163
|
+
res.end(denyBody);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Deny-then-allow precedence: an explicit deny entry beats any
|
|
168
|
+
// allow entry that would otherwise match. Operators use this for
|
|
169
|
+
// "10.0.0.0/8 EXCEPT 10.0.99.0/24" patterns.
|
|
170
|
+
for (var dii = 0; dii < deniedCidrs.length; dii++) {
|
|
171
|
+
try {
|
|
172
|
+
if (ssrfGuard.cidrContains(deniedCidrs[dii], ip)) {
|
|
173
|
+
_emitDeny(req, ip, pathname);
|
|
174
|
+
res.writeHead(denyStatus, { "Content-Type": "text/plain" });
|
|
175
|
+
res.end(denyBody);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
} catch (_e) { /* skip malformed at runtime — caught at config */ }
|
|
179
|
+
}
|
|
180
|
+
var allowed = false;
|
|
181
|
+
for (var i = 0; i < allowedCidrs.length; i++) {
|
|
182
|
+
try {
|
|
183
|
+
if (ssrfGuard.cidrContains(allowedCidrs[i], ip)) { allowed = true; break; }
|
|
184
|
+
} catch (_e) { /* skip malformed at runtime — caught at config */ }
|
|
185
|
+
}
|
|
186
|
+
if (!allowed) {
|
|
187
|
+
_emitDeny(req, ip, pathname);
|
|
188
|
+
res.writeHead(denyStatus, { "Content-Type": "text/plain" });
|
|
189
|
+
res.end(denyBody);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
return next();
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
create: create,
|
|
198
|
+
NetworkAllowlistError: NetworkAllowlistError,
|
|
199
|
+
};
|