@blamejs/core 0.7.85 → 0.7.87
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/middleware/csrf-protect.js +24 -1
- package/lib/middleware/index.js +6 -0
- package/lib/middleware/require-content-type.js +81 -0
- package/lib/middleware/require-methods.js +70 -0
- package/lib/ntp-check.js +15 -3
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.87** (2026-05-06) — Two route-guard middlewares for API hardening: `b.middleware.requireMethods` + `b.middleware.requireContentType`. **`b.middleware.requireMethods(["GET", "POST"])`** refuses any HTTP method outside the allowlist with `405 Method Not Allowed` + `Allow:` header listing the allowed methods (per RFC 9110 §15.5.6). Defends against unexpected verb routing — many CVE-class bugs trace to a route handler wired for GET that accidentally accepts arbitrary verbs (PROPFIND, OPTIONS, custom). **`b.middleware.requireContentType(["application/json"])`** refuses requests with a body (POST/PUT/PATCH by default) whose `Content-Type` isn't in the allowlist with `415 Unsupported Media Type` + `Accept:` header listing the allowed types (RFC 9110 §15.5.16). Defends against MIME-type confusion — a route that processes JSON shouldn't accept `application/x-www-form-urlencoded` even if the body parses. Both middlewares emit observability events (`middleware.requireMethods.denied` / `middleware.requireContentType.denied`) on every refusal for triage. Operators wanting to enforce content-type on idempotent verbs that DO carry bodies (rare DELETE-with-body shapes) override the default body-method list via `requireContentType(types, { methods })`.
|
|
12
|
+
|
|
13
|
+
- **0.7.86** (2026-05-06) — `b.middleware.csrfProtect({ requireJsonContentType: true })` — strict-fetch mode for JSON-only API surfaces. State-changing requests (POST/PUT/PATCH/DELETE) without `Content-Type: application/json` are refused before the token check with `CSRF: state-changing requests require Content-Type: application/json.` and audit emission `csrf.denied` with `reason: "non-JSON content-type: ..."`. The browser's form-encoded POST shape is the canonical CSRF vector — a malicious page can `<form action="/transfer" method=POST>` a victim into a state-changing request without a preflight. An `application/json` body forces a CORS preflight (the browser refuses to skip it for non-simple Content-Type values), so an attacker without an operator-allowlisted CORS origin can't reach the route at all. Default `false` — operators with HTML form submissions on the same routes (mixed SPA + classic form pages) keep current behavior; pure-fetch API operators opt in.
|
|
14
|
+
|
|
11
15
|
- **0.7.85** (2026-05-06) — `b.auth.statusList` — OAuth Token Status List (draft-ietf-oauth-status-list-20). The canonical credential-revocation mechanism for SD-JWT VC and OpenID for Verifiable Credentials. An issuer publishes a JWT-wrapped bitstring at a URL; relying parties fetch + check the bit at index N to determine if the credential whose `status_list` claim points at that URL+index is valid / invalid / suspended / application-specific. **`b.auth.statusList.create({ size, bits?, fill? })`** allocates a bit-packed buffer (`bits` ∈ {1, 2, 4, 8} per draft §6.1.1, default 1). The returned object exposes `.set(idx, status)` / `.get(idx)` / `.snapshot()` / `.toJwt({ issuer, subject, privateKey, algorithm, expiresInSec?, ... })`. **`b.auth.statusList.fromJwt(token, { publicKey | keyResolver, algorithms?, expectedIssuer?, ... })`** verifies the JWT through `b.auth.jwt.verify` and returns `{ list, claims }` — the list exposes `.get(idx)` to check status of an individual credential without decompressing into a separate Buffer. The bitstring is zlib-deflated (RFC 1951 raw deflate per draft §6.1.4) before base64url encoding so a million-entry list collapses to ~125 KB on the wire when most bits are zero. Caps the compressed payload at 1 MiB; operators publishing larger lists shard. Status constants exported as `b.auth.statusList.STATUS_{VALID,INVALID,SUSPENDED,APPLICATION_SPECIFIC}`. Foundational for the v0.7.58 EU AI Act + eIDAS 2.0 wallet slice.
|
|
12
16
|
|
|
13
17
|
- **0.7.84** (2026-05-06) — `b.crypto.sri(content, { algorithm? })` — Subresource Integrity hash builder per W3C SRI 1.0. Operators emit `<script integrity="sha384-...">` and `<link integrity="sha384-...">` to defend against CDN compromise + ISP MITM injection — the browser refuses to load a resource whose actual hash diverges from the integrity attribute. Default algorithm is `sha384` (W3C §3.2 — collision margin without sha512's 64-byte overhead); `sha256` and `sha512` also accepted, anything else refused. Accepts `Buffer` / `Uint8Array` / `string` / array of those — array inputs emit multiple space-separated integrity tokens per W3C §3.3 multi-integrity (browser picks the strongest it recognizes). Returns the standard `sha###-<base64>` format ready to paste into the `integrity=""` attribute.
|
|
@@ -228,7 +228,7 @@ function create(opts) {
|
|
|
228
228
|
|
|
229
229
|
validateOpts(opts, [
|
|
230
230
|
"cookie", "tokenLookup", "fieldName", "headerName", "methods", "audit",
|
|
231
|
-
"trustProxy", "checkOrigin", "allowedOrigins",
|
|
231
|
+
"trustProxy", "checkOrigin", "allowedOrigins", "requireJsonContentType",
|
|
232
232
|
], "middleware.csrfProtect");
|
|
233
233
|
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
234
234
|
? opts.trustProxy : false;
|
|
@@ -264,6 +264,19 @@ function create(opts) {
|
|
|
264
264
|
var allowedOrigins = Array.isArray(opts.allowedOrigins)
|
|
265
265
|
? opts.allowedOrigins.slice() : null;
|
|
266
266
|
|
|
267
|
+
// requireJsonContentType — strict-fetch mode for JSON-only API
|
|
268
|
+
// surfaces. State-changing requests without `Content-Type:
|
|
269
|
+
// application/json` are refused before the token check. The browser's
|
|
270
|
+
// form-encoded POST shape is the canonical CSRF vector — a malicious
|
|
271
|
+
// page can <form action="/transfer" method=POST> a victim into a
|
|
272
|
+
// state-changing request without a preflight; an `application/json`
|
|
273
|
+
// body forces a CORS preflight (the browser refuses to skip it for
|
|
274
|
+
// non-simple Content-Type values), so an attacker without an
|
|
275
|
+
// operator-allowlisted CORS origin can't reach the route at all.
|
|
276
|
+
// Operators with HTML form submissions on the same routes (mixed
|
|
277
|
+
// SPA + classic form pages) leave this opt-out (default).
|
|
278
|
+
var requireJsonCt = opts.requireJsonContentType === true;
|
|
279
|
+
|
|
267
280
|
// Cookie issuance config (only when opts.cookie is set).
|
|
268
281
|
var cookieCfg = null;
|
|
269
282
|
if (hasCookie) {
|
|
@@ -353,6 +366,16 @@ function create(opts) {
|
|
|
353
366
|
|
|
354
367
|
if (methods.indexOf(req.method) === -1) return next();
|
|
355
368
|
|
|
369
|
+
// requireJsonContentType — refuse before the token check.
|
|
370
|
+
if (requireJsonCt) {
|
|
371
|
+
var ct = req.headers && req.headers["content-type"];
|
|
372
|
+
var bare = (typeof ct === "string" ? ct.split(";")[0].trim().toLowerCase() : "");
|
|
373
|
+
if (bare !== "application/json") {
|
|
374
|
+
_emitDenied(req, "non-JSON content-type: " + (bare || "<absent>"));
|
|
375
|
+
return _writeReject(res, "CSRF: state-changing requests require Content-Type: application/json.");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
356
379
|
// Origin / Referer cross-check (defense-in-depth alongside the
|
|
357
380
|
// double-submit token). Refuses cross-origin state-changing
|
|
358
381
|
// requests even when the token is valid (e.g. operator-mistaken
|
package/lib/middleware/index.js
CHANGED
|
@@ -40,6 +40,8 @@ var requestId = require("./request-id");
|
|
|
40
40
|
var requestLog = require("./request-log");
|
|
41
41
|
var requireAal = require("./require-aal");
|
|
42
42
|
var requireAuth = require("./require-auth");
|
|
43
|
+
var requireContentType = require("./require-content-type");
|
|
44
|
+
var requireMethods = require("./require-methods");
|
|
43
45
|
var securityHeaders = require("./security-headers");
|
|
44
46
|
var securityTxt = require("./security-txt");
|
|
45
47
|
var sse = require("./sse");
|
|
@@ -55,6 +57,8 @@ module.exports = {
|
|
|
55
57
|
bearerAuth: bearerAuth.create,
|
|
56
58
|
requireAal: requireAal.create,
|
|
57
59
|
requireAuth: requireAuth.create,
|
|
60
|
+
requireContentType: requireContentType.create,
|
|
61
|
+
requireMethods: requireMethods.create,
|
|
58
62
|
csrfProtect: csrfProtect.create,
|
|
59
63
|
fetchMetadata: fetchMetadata.create,
|
|
60
64
|
gpc: gpc.create,
|
|
@@ -85,6 +89,8 @@ module.exports = {
|
|
|
85
89
|
bearerAuth: bearerAuth,
|
|
86
90
|
requireAal: requireAal,
|
|
87
91
|
requireAuth: requireAuth,
|
|
92
|
+
requireContentType: requireContentType,
|
|
93
|
+
requireMethods: requireMethods,
|
|
88
94
|
csrfProtect: csrfProtect,
|
|
89
95
|
fetchMetadata: fetchMetadata,
|
|
90
96
|
bodyParser: bodyParser,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* require-content-type middleware — refuses requests with a body
|
|
4
|
+
* (POST/PUT/PATCH) whose `Content-Type` header isn't in the
|
|
5
|
+
* operator-supplied allowlist.
|
|
6
|
+
*
|
|
7
|
+
* Defense against MIME-type confusion: a route that processes JSON
|
|
8
|
+
* shouldn't accept `application/x-www-form-urlencoded` even if the
|
|
9
|
+
* body parses (and vice versa). The middleware refuses with 415
|
|
10
|
+
* before the body parser runs, per RFC 9110 §15.5.16.
|
|
11
|
+
*
|
|
12
|
+
* router.use(b.middleware.requireContentType(["application/json"]));
|
|
13
|
+
*
|
|
14
|
+
* GET / HEAD / DELETE / OPTIONS without a body bypass the check by
|
|
15
|
+
* default. Operators wanting to enforce content-type on idempotent
|
|
16
|
+
* verbs that DO carry bodies (rare DELETE-with-body shapes) pass
|
|
17
|
+
* `methods` to override.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
var lazyRequire = require("../lazy-require");
|
|
21
|
+
var { defineClass } = require("../framework-error");
|
|
22
|
+
|
|
23
|
+
var RequireContentTypeError = defineClass("RequireContentTypeError", { alwaysPermanent: true });
|
|
24
|
+
|
|
25
|
+
var observability = lazyRequire(function () { return require("../observability"); });
|
|
26
|
+
|
|
27
|
+
var DEFAULT_BODY_METHODS = ["POST", "PUT", "PATCH"];
|
|
28
|
+
|
|
29
|
+
function _normalizeAllowed(types) {
|
|
30
|
+
if (!Array.isArray(types) || types.length === 0) return null;
|
|
31
|
+
var out = [];
|
|
32
|
+
for (var i = 0; i < types.length; i += 1) {
|
|
33
|
+
var t = types[i];
|
|
34
|
+
if (typeof t !== "string" || t.length === 0) return null;
|
|
35
|
+
var bare = t.split(";")[0].trim().toLowerCase();
|
|
36
|
+
if (bare.length === 0) return null;
|
|
37
|
+
out.push(bare);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function create(allowed, opts) {
|
|
43
|
+
var normalized = _normalizeAllowed(allowed);
|
|
44
|
+
if (!normalized) {
|
|
45
|
+
throw new RequireContentTypeError("require-content-type/no-allowlist",
|
|
46
|
+
"middleware.requireContentType: first argument must be a non-empty array of content-type strings");
|
|
47
|
+
}
|
|
48
|
+
opts = opts || {};
|
|
49
|
+
var methods = Array.isArray(opts.methods) && opts.methods.length > 0
|
|
50
|
+
? opts.methods.map(function (m) { return m.toUpperCase(); })
|
|
51
|
+
: DEFAULT_BODY_METHODS.slice();
|
|
52
|
+
var auditOn = opts.audit !== false;
|
|
53
|
+
|
|
54
|
+
return function requireContentTypeMiddleware(req, res, next) {
|
|
55
|
+
var m = (req.method || "").toUpperCase();
|
|
56
|
+
if (methods.indexOf(m) === -1) return next();
|
|
57
|
+
var ct = req.headers && req.headers["content-type"];
|
|
58
|
+
var bare = (typeof ct === "string" ? ct.split(";")[0].trim().toLowerCase() : "");
|
|
59
|
+
if (bare.length > 0 && normalized.indexOf(bare) !== -1) return next();
|
|
60
|
+
if (!res.headersSent) {
|
|
61
|
+
var body = "Unsupported Media Type";
|
|
62
|
+
res.writeHead(415, { // allow:raw-byte-literal — HTTP 415 status
|
|
63
|
+
"Accept": normalized.join(", "),
|
|
64
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
65
|
+
"Content-Length": Buffer.byteLength(body),
|
|
66
|
+
});
|
|
67
|
+
res.end(body);
|
|
68
|
+
}
|
|
69
|
+
if (auditOn) {
|
|
70
|
+
try {
|
|
71
|
+
observability().safeEvent("middleware.requireContentType.denied", 1, {
|
|
72
|
+
method: m, contentType: bare || "<absent>", route: req.url,
|
|
73
|
+
});
|
|
74
|
+
} catch (_e) { /* drop-silent */ }
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
create: create,
|
|
81
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* require-methods middleware — refuses HTTP methods outside an
|
|
4
|
+
* operator-supplied allowlist.
|
|
5
|
+
*
|
|
6
|
+
* Defense against unexpected verb routing. Many CVE-class bugs trace
|
|
7
|
+
* to a route handler that was wired for GET but accidentally also
|
|
8
|
+
* accepts arbitrary verbs (PROPFIND, OPTIONS, custom). Mounting
|
|
9
|
+
* `requireMethods(["GET", "POST"])` on the route blocks anything
|
|
10
|
+
* outside the allowlist before the handler sees the request.
|
|
11
|
+
*
|
|
12
|
+
* router.use(b.middleware.requireMethods(["GET", "POST"]));
|
|
13
|
+
*
|
|
14
|
+
* Refusal returns 405 with `Allow:` listing the allowed methods, per
|
|
15
|
+
* RFC 9110 §15.5.6.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
var lazyRequire = require("../lazy-require");
|
|
19
|
+
var { defineClass } = require("../framework-error");
|
|
20
|
+
|
|
21
|
+
var RequireMethodsError = defineClass("RequireMethodsError", { alwaysPermanent: true });
|
|
22
|
+
|
|
23
|
+
var observability = lazyRequire(function () { return require("../observability"); });
|
|
24
|
+
|
|
25
|
+
function create(allowed, opts) {
|
|
26
|
+
if (!Array.isArray(allowed) || allowed.length === 0) {
|
|
27
|
+
throw new RequireMethodsError("require-methods/no-allowlist",
|
|
28
|
+
"middleware.requireMethods: first argument must be a non-empty array of HTTP methods");
|
|
29
|
+
}
|
|
30
|
+
var normalized = [];
|
|
31
|
+
for (var i = 0; i < allowed.length; i += 1) {
|
|
32
|
+
if (typeof allowed[i] !== "string" || allowed[i].length === 0) {
|
|
33
|
+
throw new RequireMethodsError("require-methods/bad-method",
|
|
34
|
+
"middleware.requireMethods: method[" + i + "] must be a non-empty string");
|
|
35
|
+
}
|
|
36
|
+
if (/[\r\n\0\s,;]/.test(allowed[i])) {
|
|
37
|
+
throw new RequireMethodsError("require-methods/bad-method",
|
|
38
|
+
"middleware.requireMethods: method[" + i + "] contains forbidden whitespace / separator characters");
|
|
39
|
+
}
|
|
40
|
+
normalized.push(allowed[i].toUpperCase());
|
|
41
|
+
}
|
|
42
|
+
var allowHeader = normalized.join(", ");
|
|
43
|
+
opts = opts || {};
|
|
44
|
+
var auditOn = opts.audit !== false;
|
|
45
|
+
|
|
46
|
+
return function requireMethodsMiddleware(req, res, next) {
|
|
47
|
+
var m = (req.method || "").toUpperCase();
|
|
48
|
+
if (normalized.indexOf(m) !== -1) return next();
|
|
49
|
+
if (!res.headersSent) {
|
|
50
|
+
var body = "Method Not Allowed";
|
|
51
|
+
res.writeHead(405, { // allow:raw-byte-literal — HTTP 405 status
|
|
52
|
+
"Allow": allowHeader,
|
|
53
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
54
|
+
"Content-Length": Buffer.byteLength(body),
|
|
55
|
+
});
|
|
56
|
+
res.end(body);
|
|
57
|
+
}
|
|
58
|
+
if (auditOn) {
|
|
59
|
+
try {
|
|
60
|
+
observability().safeEvent("middleware.requireMethods.denied", 1, {
|
|
61
|
+
method: m, route: req.url,
|
|
62
|
+
});
|
|
63
|
+
} catch (_e) { /* drop-silent */ }
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
create: create,
|
|
70
|
+
};
|
package/lib/ntp-check.js
CHANGED
|
@@ -124,10 +124,22 @@ function querySingle(server, opts) {
|
|
|
124
124
|
return done({ code: "ntp/bad-reply", message: "reply too short (" + (msg && msg.length) + " bytes)" });
|
|
125
125
|
}
|
|
126
126
|
// Bytes 40-47 = Transmit Timestamp (NTP epoch seconds.fraction)
|
|
127
|
-
var ntpSeconds = msg.readUInt32BE(40);
|
|
128
|
-
var ntpFraction = msg.readUInt32BE(44);
|
|
127
|
+
var ntpSeconds = msg.readUInt32BE(40); // allow:raw-byte-literal — NTP packet offset
|
|
128
|
+
var ntpFraction = msg.readUInt32BE(44); // allow:raw-byte-literal — NTP packet offset
|
|
129
|
+
// Refuse a reply whose Transmit Timestamp is zero or earlier than
|
|
130
|
+
// the NTP epoch (1900-01-01). RFC 5905 §7.3 — a Stratum-16
|
|
131
|
+
// unsynchronized server emits 0 here; fed to the Unix-offset
|
|
132
|
+
// subtraction it produces a large-negative serverUnixSeconds
|
|
133
|
+
// that crashes downstream C.TIME helpers (which require non-
|
|
134
|
+
// negative finite). Treat as "unsynchronized peer — no drift
|
|
135
|
+
// measurement possible" rather than throw out of the dgram
|
|
136
|
+
// 'message' handler.
|
|
137
|
+
if (ntpSeconds < NTP_TO_UNIX_OFFSET_SECONDS) {
|
|
138
|
+
return done({ code: "ntp/unsynchronized",
|
|
139
|
+
message: "server returned NTP transmit timestamp < Unix epoch (likely Stratum-16 unsynchronized)" });
|
|
140
|
+
}
|
|
129
141
|
var serverUnixSeconds = ntpSeconds - NTP_TO_UNIX_OFFSET_SECONDS;
|
|
130
|
-
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000));
|
|
142
|
+
var fracMs = Math.round(C.TIME.seconds(ntpFraction / 0x100000000)); // allow:raw-byte-literal — NTP fraction divisor (2^32)
|
|
131
143
|
var serverTimeMs = C.TIME.seconds(serverUnixSeconds) + fracMs;
|
|
132
144
|
|
|
133
145
|
// Round-trip-corrected drift: assume the server's reply transmit
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:96ee5394-43c1-471e-98e9-f3b59c96f3e0",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-06T06:
|
|
8
|
+
"timestamp": "2026-05-06T06:37:03.759Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.87",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.87",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.87",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.87",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|