@blamejs/core 0.6.70 → 0.7.0
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 +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +23 -26
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +4 -3
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +26 -15
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
|
@@ -114,11 +114,14 @@ var path = require("path");
|
|
|
114
114
|
var nodeCrypto = require("node:crypto");
|
|
115
115
|
var atomicFile = require("../atomic-file");
|
|
116
116
|
var crypto = require("../crypto");
|
|
117
|
+
var requestHelpers = require("../request-helpers");
|
|
118
|
+
var safeBuffer = require("../safe-buffer");
|
|
117
119
|
var safeJson = require("../safe-json");
|
|
118
120
|
var validateOpts = require("../validate-opts");
|
|
119
121
|
var C = require("../constants");
|
|
120
122
|
var { defineClass } = require("../framework-error");
|
|
121
123
|
|
|
124
|
+
var HTTP_STATUS = requestHelpers.HTTP_STATUS;
|
|
122
125
|
var BodyParserError = defineClass("BodyParserError", { withStatusCode: true });
|
|
123
126
|
|
|
124
127
|
// Mirrors safe-json.js + safe-schema.js. Field names that match these
|
|
@@ -167,6 +170,11 @@ var DEFAULTS = Object.freeze({
|
|
|
167
170
|
|
|
168
171
|
var BODY_BEARING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
169
172
|
|
|
173
|
+
// Headroom on top of arrayLimit before declaring a key-bomb. Sized so a
|
|
174
|
+
// reasonable form (a few hundred fields with repeated names) fits without
|
|
175
|
+
// tripping the cap.
|
|
176
|
+
var URLENCODED_KEY_HEADROOM = C.BYTES.bytes(1000);
|
|
177
|
+
|
|
170
178
|
// ---- helpers ----
|
|
171
179
|
|
|
172
180
|
function _contentType(req) {
|
|
@@ -255,7 +263,7 @@ function _bufferBody(req, limit) {
|
|
|
255
263
|
reject(new BodyParserError(
|
|
256
264
|
"body-parser/bad-content-length",
|
|
257
265
|
"Content-Length is not a sequence of decimal digits: " + JSON.stringify(cl),
|
|
258
|
-
true,
|
|
266
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
259
267
|
));
|
|
260
268
|
return;
|
|
261
269
|
}
|
|
@@ -263,31 +271,30 @@ function _bufferBody(req, limit) {
|
|
|
263
271
|
reject(new BodyParserError(
|
|
264
272
|
"body-parser/too-large",
|
|
265
273
|
"request body exceeds limit (" + clNum + " > " + limit + ")",
|
|
266
|
-
true,
|
|
274
|
+
true, HTTP_STATUS.PAYLOAD_TOO_LARGE
|
|
267
275
|
));
|
|
268
276
|
return;
|
|
269
277
|
}
|
|
270
278
|
}
|
|
271
|
-
var
|
|
272
|
-
|
|
279
|
+
var collector = safeBuffer.boundedChunkCollector({
|
|
280
|
+
maxBytes: limit,
|
|
281
|
+
errorClass: BodyParserError,
|
|
282
|
+
sizeCode: "body-parser/too-large",
|
|
283
|
+
sizeMessage: "request body exceeds limit",
|
|
284
|
+
});
|
|
273
285
|
var done = false;
|
|
274
286
|
req.on("data", function (chunk) {
|
|
275
287
|
if (done) return;
|
|
276
|
-
|
|
277
|
-
|
|
288
|
+
try { collector.push(chunk); }
|
|
289
|
+
catch (e) {
|
|
278
290
|
done = true;
|
|
279
291
|
try { req.destroy(); } catch (_e) { /* socket already closed */ }
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"request body exceeds limit (streamed " + total + " > " + limit + ")",
|
|
283
|
-
true, 413
|
|
284
|
-
);
|
|
285
|
-
reject(err);
|
|
292
|
+
if (e && e.isBodyParserError) e.statusCode = HTTP_STATUS.PAYLOAD_TOO_LARGE;
|
|
293
|
+
reject(e);
|
|
286
294
|
return;
|
|
287
295
|
}
|
|
288
|
-
chunks.push(chunk);
|
|
289
296
|
});
|
|
290
|
-
req.on("end", function () { if (!done) { done = true; resolve(
|
|
297
|
+
req.on("end", function () { if (!done) { done = true; resolve(collector.result()); } });
|
|
291
298
|
req.on("error", function (e) { if (!done) { done = true; reject(e); } });
|
|
292
299
|
});
|
|
293
300
|
}
|
|
@@ -304,7 +311,7 @@ async function _parseJson(req, opts) {
|
|
|
304
311
|
throw new BodyParserError(
|
|
305
312
|
"body-parser/json-strict",
|
|
306
313
|
"JSON body must start with '{' or '[' (strict mode)",
|
|
307
|
-
true,
|
|
314
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
308
315
|
);
|
|
309
316
|
}
|
|
310
317
|
}
|
|
@@ -316,7 +323,7 @@ async function _parseJson(req, opts) {
|
|
|
316
323
|
throw new BodyParserError(
|
|
317
324
|
"body-parser/json-malformed",
|
|
318
325
|
"JSON parse failed: " + ((e && e.message) || String(e)),
|
|
319
|
-
true,
|
|
326
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
320
327
|
);
|
|
321
328
|
}
|
|
322
329
|
if (typeof opts.parseHook === "function") {
|
|
@@ -325,7 +332,7 @@ async function _parseJson(req, opts) {
|
|
|
325
332
|
throw new BodyParserError(
|
|
326
333
|
"body-parser/json-hook",
|
|
327
334
|
"JSON parseHook failed: " + ((e && e.message) || String(e)),
|
|
328
|
-
true,
|
|
335
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
329
336
|
);
|
|
330
337
|
}
|
|
331
338
|
}
|
|
@@ -344,7 +351,7 @@ async function _parseUrlencoded(req, opts) {
|
|
|
344
351
|
throw new BodyParserError(
|
|
345
352
|
"body-parser/urlencoded-malformed",
|
|
346
353
|
"urlencoded parse failed: " + ((e && e.message) || String(e)),
|
|
347
|
-
true,
|
|
354
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
348
355
|
);
|
|
349
356
|
}
|
|
350
357
|
var out = {};
|
|
@@ -360,11 +367,11 @@ async function _parseUrlencoded(req, opts) {
|
|
|
360
367
|
throw new BodyParserError(
|
|
361
368
|
"body-parser/urlencoded-poisoned-key",
|
|
362
369
|
"urlencoded body contains forbidden key '" + k + "' (prototype-pollution defense)",
|
|
363
|
-
true,
|
|
370
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
364
371
|
);
|
|
365
372
|
}
|
|
366
373
|
keyCount++;
|
|
367
|
-
if (keyCount > opts.arrayLimit +
|
|
374
|
+
if (keyCount > opts.arrayLimit + URLENCODED_KEY_HEADROOM) { // soft cap on total keys
|
|
368
375
|
throw new BodyParserError(
|
|
369
376
|
"body-parser/urlencoded-too-many-fields",
|
|
370
377
|
"urlencoded body has too many fields",
|
|
@@ -486,7 +493,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
486
493
|
throw new BodyParserError(
|
|
487
494
|
"body-parser/multipart-no-boundary",
|
|
488
495
|
"multipart Content-Type missing boundary parameter",
|
|
489
|
-
true,
|
|
496
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
490
497
|
);
|
|
491
498
|
}
|
|
492
499
|
// Resolve tmpDir per-request so directory-creation failure surfaces as a
|
|
@@ -542,7 +549,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
542
549
|
currentFilename = null;
|
|
543
550
|
currentMime = null;
|
|
544
551
|
currentTmpPath = null;
|
|
545
|
-
if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) {} currentFd = null; }
|
|
552
|
+
if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ } currentFd = null; }
|
|
546
553
|
currentSize = 0;
|
|
547
554
|
currentHash = null;
|
|
548
555
|
currentBuf = null;
|
|
@@ -571,10 +578,10 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
571
578
|
}
|
|
572
579
|
|
|
573
580
|
function _cleanup() {
|
|
574
|
-
if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) {} currentFd = null; }
|
|
575
|
-
if (currentTmpPath) { try { fs.unlinkSync(currentTmpPath); } catch (_e) {} }
|
|
581
|
+
if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ } currentFd = null; }
|
|
582
|
+
if (currentTmpPath) { try { fs.unlinkSync(currentTmpPath); } catch (_e) { /* tmp file already removed */ } }
|
|
576
583
|
for (var i = 0; i < files.length; i++) {
|
|
577
|
-
try { fs.unlinkSync(files[i].path); } catch (_e) {}
|
|
584
|
+
try { fs.unlinkSync(files[i].path); } catch (_e) { /* tmp file already removed */ }
|
|
578
585
|
}
|
|
579
586
|
}
|
|
580
587
|
|
|
@@ -632,14 +639,14 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
632
639
|
continue;
|
|
633
640
|
}
|
|
634
641
|
done(new BodyParserError("body-parser/multipart-malformed",
|
|
635
|
-
"expected --, \\r\\n, or \\n after boundary", true,
|
|
642
|
+
"expected --, \\r\\n, or \\n after boundary", true, HTTP_STATUS.BAD_REQUEST));
|
|
636
643
|
return;
|
|
637
644
|
}
|
|
638
645
|
if (state === MP_HEADERS) {
|
|
639
646
|
// Read until \r\n\r\n.
|
|
640
647
|
var headEnd = pending.indexOf("\r\n\r\n");
|
|
641
648
|
if (headEnd === -1) {
|
|
642
|
-
if (pending.length >
|
|
649
|
+
if (pending.length > C.BYTES.kib(16)) {
|
|
643
650
|
done(new BodyParserError("body-parser/multipart-headers-too-large",
|
|
644
651
|
"multipart part headers exceed 16KB", true, 413));
|
|
645
652
|
return;
|
|
@@ -652,13 +659,13 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
652
659
|
var cd = _parseHeaderParams(currentHeaders["content-disposition"]);
|
|
653
660
|
if (cd._value !== "form-data" || typeof cd.name !== "string" || cd.name.length === 0) {
|
|
654
661
|
done(new BodyParserError("body-parser/multipart-bad-disposition",
|
|
655
|
-
"multipart part missing form-data Content-Disposition", true,
|
|
662
|
+
"multipart part missing form-data Content-Disposition", true, HTTP_STATUS.BAD_REQUEST));
|
|
656
663
|
return;
|
|
657
664
|
}
|
|
658
665
|
if (POISONED_KEYS.has(cd.name)) {
|
|
659
666
|
done(new BodyParserError("body-parser/multipart-poisoned-field",
|
|
660
667
|
"multipart field '" + cd.name + "' is forbidden (prototype-pollution defense)",
|
|
661
|
-
true,
|
|
668
|
+
true, HTTP_STATUS.BAD_REQUEST));
|
|
662
669
|
return;
|
|
663
670
|
}
|
|
664
671
|
currentField = cd.name;
|
|
@@ -667,7 +674,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
667
674
|
if (!currentFilename) {
|
|
668
675
|
done(new BodyParserError("body-parser/multipart-bad-filename",
|
|
669
676
|
"multipart part filename did not survive sanitization (path traversal or empty)",
|
|
670
|
-
true,
|
|
677
|
+
true, HTTP_STATUS.BAD_REQUEST));
|
|
671
678
|
return;
|
|
672
679
|
}
|
|
673
680
|
currentMime = currentHeaders["content-type"] || "application/octet-stream";
|
|
@@ -736,7 +743,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
736
743
|
|
|
737
744
|
// Generate the tmp path — never derived from the
|
|
738
745
|
// operator-supplied filename.
|
|
739
|
-
var unique = crypto.generateToken(16);
|
|
746
|
+
var unique = crypto.generateToken(C.BYTES.bytes(16));
|
|
740
747
|
currentTmpPath = path.join(tmpDir, "blamejs-up-" + unique);
|
|
741
748
|
try {
|
|
742
749
|
currentFd = fs.openSync(currentTmpPath, "wx", 0o600);
|
|
@@ -859,7 +866,7 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
859
866
|
// fileFilter rejected — already recorded in filesRejected; no
|
|
860
867
|
// tmp file was opened, nothing to clean up here.
|
|
861
868
|
} else if (currentFd !== null) {
|
|
862
|
-
try { fs.closeSync(currentFd); } catch (_e) {}
|
|
869
|
+
try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ }
|
|
863
870
|
currentFd = null;
|
|
864
871
|
files.push({
|
|
865
872
|
field: currentField,
|
|
@@ -912,14 +919,14 @@ async function _parseMultipart(req, opts, ctParams) {
|
|
|
912
919
|
if (resolved) return;
|
|
913
920
|
if (state !== MP_DONE) {
|
|
914
921
|
done(new BodyParserError("body-parser/multipart-truncated",
|
|
915
|
-
"multipart stream ended before final boundary", true,
|
|
922
|
+
"multipart stream ended before final boundary", true, HTTP_STATUS.BAD_REQUEST));
|
|
916
923
|
return;
|
|
917
924
|
}
|
|
918
925
|
});
|
|
919
926
|
req.on("error", function (e) {
|
|
920
927
|
if (resolved) return;
|
|
921
928
|
done(new BodyParserError("body-parser/multipart-stream",
|
|
922
|
-
"multipart stream error: " + ((e && e.message) || String(e)), true,
|
|
929
|
+
"multipart stream error: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST));
|
|
923
930
|
});
|
|
924
931
|
});
|
|
925
932
|
} catch (e) {
|
|
@@ -981,7 +988,7 @@ function create(opts) {
|
|
|
981
988
|
if (cleanedUp) return;
|
|
982
989
|
cleanedUp = true;
|
|
983
990
|
for (var i = 0; i < mpResult.files.length; i++) {
|
|
984
|
-
try { fs.unlinkSync(mpResult.files[i].path); } catch (_e) {}
|
|
991
|
+
try { fs.unlinkSync(mpResult.files[i].path); } catch (_e) { /* tmp file already removed */ }
|
|
985
992
|
}
|
|
986
993
|
}
|
|
987
994
|
res.on("finish", cleanup);
|
|
@@ -1006,7 +1013,7 @@ function create(opts) {
|
|
|
1006
1013
|
"body-parser/unsupported-content-type"
|
|
1007
1014
|
);
|
|
1008
1015
|
} catch (e) {
|
|
1009
|
-
var status = (e && typeof e.statusCode === "number") ? e.statusCode :
|
|
1016
|
+
var status = (e && typeof e.statusCode === "number") ? e.statusCode : HTTP_STATUS.BAD_REQUEST;
|
|
1010
1017
|
var code = (e && typeof e.code === "string") ? e.code : "body-parser/error";
|
|
1011
1018
|
var message = (e && e.message) ? e.message : String(e);
|
|
1012
1019
|
_writeError(res, status, message, code);
|
|
@@ -1022,20 +1029,20 @@ async function _parseJsonFromBuf(buf, opts) {
|
|
|
1022
1029
|
var head = text.replace(/^[\s\u00A0\uFEFF]+/, "")[0];
|
|
1023
1030
|
if (head !== "{" && head !== "[") {
|
|
1024
1031
|
throw new BodyParserError("body-parser/json-strict",
|
|
1025
|
-
"JSON body must start with '{' or '[' (strict mode)", true,
|
|
1032
|
+
"JSON body must start with '{' or '[' (strict mode)", true, HTTP_STATUS.BAD_REQUEST);
|
|
1026
1033
|
}
|
|
1027
1034
|
}
|
|
1028
1035
|
var parsed;
|
|
1029
1036
|
try { parsed = safeJson.parse(text, { maxBytes: opts.limit }); }
|
|
1030
1037
|
catch (e) {
|
|
1031
1038
|
throw new BodyParserError("body-parser/json-malformed",
|
|
1032
|
-
"JSON parse failed: " + ((e && e.message) || String(e)), true,
|
|
1039
|
+
"JSON parse failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
|
|
1033
1040
|
}
|
|
1034
1041
|
if (typeof opts.parseHook === "function") {
|
|
1035
1042
|
try { parsed = opts.parseHook(parsed); }
|
|
1036
1043
|
catch (e) {
|
|
1037
1044
|
throw new BodyParserError("body-parser/json-hook",
|
|
1038
|
-
"JSON parseHook failed: " + ((e && e.message) || String(e)), true,
|
|
1045
|
+
"JSON parseHook failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
|
|
1039
1046
|
}
|
|
1040
1047
|
}
|
|
1041
1048
|
return parsed;
|
|
@@ -47,8 +47,25 @@ var DEFAULT_BLOCKED_AGENTS = [
|
|
|
47
47
|
var lazyRequire = require("../lazy-require");
|
|
48
48
|
var requestHelpers = require("../request-helpers");
|
|
49
49
|
var validateOpts = require("../validate-opts");
|
|
50
|
+
var { defineClass } = require("../framework-error");
|
|
50
51
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
51
52
|
|
|
53
|
+
var BotGuardError = defineClass("BotGuardError", { alwaysPermanent: true });
|
|
54
|
+
|
|
55
|
+
// allowedAgents / blockedAgents are operator-supplied at create() time
|
|
56
|
+
// (NOT request bytes). The framework requires RegExp instances rather
|
|
57
|
+
// than strings — string-to-regex compilation from an operator-supplied
|
|
58
|
+
// value is an avoidable ReDoS vector at framework boot. Operators
|
|
59
|
+
// constructing patterns dynamically compile at their own call site so
|
|
60
|
+
// the pattern source is visible in their code.
|
|
61
|
+
function _coerceAgentPattern(r, where) {
|
|
62
|
+
if (r instanceof RegExp) return r;
|
|
63
|
+
throw new BotGuardError("bot-guard/bad-pattern",
|
|
64
|
+
where + " must be a RegExp instance; got " + (typeof r) +
|
|
65
|
+
" (compile the pattern at the call site so the source is visible " +
|
|
66
|
+
"in operator code)");
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
// Bot-guard's "trust the proxy header" semantics for actor.ip — the
|
|
53
70
|
// audit event records the apparent source even when behind a CDN, but
|
|
54
71
|
// only when the operator opts in to trustProxy. Without the opt, we
|
|
@@ -71,8 +88,12 @@ function create(opts) {
|
|
|
71
88
|
var _xffIp = _xffIpFor(trustProxy);
|
|
72
89
|
var mode = opts.mode || "block";
|
|
73
90
|
var onlyForHtml = opts.onlyForHtml !== false;
|
|
74
|
-
var allowedAgents = (opts.allowedAgents || []).map(function (r) {
|
|
75
|
-
|
|
91
|
+
var allowedAgents = (opts.allowedAgents || []).map(function (r, i) {
|
|
92
|
+
return _coerceAgentPattern(r, "middleware.botGuard: allowedAgents[" + i + "]");
|
|
93
|
+
});
|
|
94
|
+
var blockedAgents = DEFAULT_BLOCKED_AGENTS.concat((opts.blockedAgents || []).map(function (r, i) {
|
|
95
|
+
return _coerceAgentPattern(r, "middleware.botGuard: blockedAgents[" + i + "]");
|
|
96
|
+
}));
|
|
76
97
|
var skipPaths = opts.skipPaths || [];
|
|
77
98
|
var statusOnBlock = opts.statusOnBlock || 403;
|
|
78
99
|
var bodyOnBlock = opts.bodyOnBlock !== undefined ? opts.bodyOnBlock : "Forbidden";
|
|
@@ -127,7 +148,7 @@ function create(opts) {
|
|
|
127
148
|
metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
|
|
128
149
|
requestId: req.requestId,
|
|
129
150
|
});
|
|
130
|
-
} catch (_e) {}
|
|
151
|
+
} catch (_e) { /* audit best-effort */ }
|
|
131
152
|
return next();
|
|
132
153
|
}
|
|
133
154
|
|
|
@@ -140,7 +161,7 @@ function create(opts) {
|
|
|
140
161
|
metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
|
|
141
162
|
requestId: req.requestId,
|
|
142
163
|
});
|
|
143
|
-
} catch (_e) {}
|
|
164
|
+
} catch (_e) { /* audit best-effort */ }
|
|
144
165
|
|
|
145
166
|
if (res.writableEnded) return;
|
|
146
167
|
if (typeof res.writeHead === "function") {
|
|
@@ -89,6 +89,8 @@
|
|
|
89
89
|
*/
|
|
90
90
|
|
|
91
91
|
var zlib = require("node:zlib");
|
|
92
|
+
var C = require("../constants");
|
|
93
|
+
var nb = require("../numeric-bounds");
|
|
92
94
|
var requestHelpers = require("../request-helpers");
|
|
93
95
|
var validateOpts = require("../validate-opts");
|
|
94
96
|
var { defineClass } = require("../framework-error");
|
|
@@ -96,7 +98,7 @@ var { defineClass } = require("../framework-error");
|
|
|
96
98
|
var CompressionError = defineClass("CompressionError", { alwaysPermanent: true });
|
|
97
99
|
|
|
98
100
|
var DEFAULT_OPTS = Object.freeze({
|
|
99
|
-
threshold:
|
|
101
|
+
threshold: C.BYTES.kib(1),
|
|
100
102
|
encodings: ["br", "gzip"],
|
|
101
103
|
contentTypes: [
|
|
102
104
|
"text/*",
|
|
@@ -116,8 +118,13 @@ var DEFAULT_OPTS = Object.freeze({
|
|
|
116
118
|
// the encodings default.
|
|
117
119
|
var SUPPORTED_ENCODINGS = new Set(["br", "gzip"]);
|
|
118
120
|
|
|
121
|
+
var HTTP_STATUS = requestHelpers.HTTP_STATUS;
|
|
122
|
+
// 205 Reset Content is rarely-used — local hex constant rather than
|
|
123
|
+
// expanding the framework's HTTP_STATUS table for a single call site.
|
|
124
|
+
var HTTP_RESET_CONTENT = 0xCD;
|
|
125
|
+
|
|
119
126
|
// Status codes that NEVER carry a body — RFC 7230 §3.3.3.
|
|
120
|
-
var NO_BODY_STATUS = new Set([
|
|
127
|
+
var NO_BODY_STATUS = new Set([HTTP_STATUS.NO_CONTENT, HTTP_RESET_CONTENT, HTTP_STATUS.NOT_MODIFIED]);
|
|
121
128
|
|
|
122
129
|
// Parse Accept-Encoding into [{ encoding, q }] sorted by q descending.
|
|
123
130
|
// Built on the shared `requestHelpers.parseQualityList` (RFC 9110
|
|
@@ -224,8 +231,16 @@ function create(opts) {
|
|
|
224
231
|
"threshold", "encodings", "contentTypes",
|
|
225
232
|
"gzipLevel", "brotliQuality", "filter",
|
|
226
233
|
], "middleware.compression");
|
|
227
|
-
var threshold
|
|
228
|
-
|
|
234
|
+
var threshold;
|
|
235
|
+
if (opts.threshold === undefined) {
|
|
236
|
+
threshold = DEFAULT_OPTS.threshold;
|
|
237
|
+
} else if (nb.isNonNegativeFiniteInt(opts.threshold)) {
|
|
238
|
+
threshold = opts.threshold;
|
|
239
|
+
} else {
|
|
240
|
+
throw new CompressionError("compression/bad-opt",
|
|
241
|
+
"middleware.compression: threshold must be a non-negative finite integer; got " +
|
|
242
|
+
nb.shape(opts.threshold));
|
|
243
|
+
}
|
|
229
244
|
var encodings = Array.isArray(opts.encodings) && opts.encodings.length > 0
|
|
230
245
|
? opts.encodings.slice() : DEFAULT_OPTS.encodings.slice();
|
|
231
246
|
var contentTypes = Array.isArray(opts.contentTypes) && opts.contentTypes.length > 0
|
|
@@ -245,7 +260,7 @@ function create(opts) {
|
|
|
245
260
|
return function compression(req, res, next) {
|
|
246
261
|
if (filter) {
|
|
247
262
|
try { if (!filter(req, res)) return next(); }
|
|
248
|
-
catch (_e) { return next(); }
|
|
263
|
+
catch (_e) { /* operator filter threw — fall through and skip compression */ return next(); }
|
|
249
264
|
}
|
|
250
265
|
|
|
251
266
|
// Negotiate encoding up-front from Accept-Encoding. If the client
|
|
@@ -313,7 +328,7 @@ function create(opts) {
|
|
|
313
328
|
// On compressor error the response is already half-written;
|
|
314
329
|
// there's not much we can do beyond closing the underlying
|
|
315
330
|
// socket. Best-effort end the response.
|
|
316
|
-
try { originalEnd.call(res); } catch (_e) {}
|
|
331
|
+
try { originalEnd.call(res); } catch (_e) { /* response already ended */ }
|
|
317
332
|
});
|
|
318
333
|
// Forward drain. Without this, source streams piped into our
|
|
319
334
|
// wrapped res (e.g. fs.createReadStream from staticServe) stall:
|
|
@@ -353,12 +368,12 @@ function create(opts) {
|
|
|
353
368
|
headersObj["Vary"] = _appendVary(existingVary, "Accept-Encoding");
|
|
354
369
|
} else {
|
|
355
370
|
if (originalRemoveHdr) {
|
|
356
|
-
try { originalRemoveHdr("Content-Length"); } catch (_e) {}
|
|
371
|
+
try { originalRemoveHdr("Content-Length"); } catch (_e) { /* header may not be set */ }
|
|
357
372
|
}
|
|
358
373
|
if (originalSetHeader) {
|
|
359
|
-
try { originalSetHeader("Content-Encoding", encoding); } catch (_e) {}
|
|
374
|
+
try { originalSetHeader("Content-Encoding", encoding); } catch (_e) { /* headers already sent */ }
|
|
360
375
|
var existing = originalGetHeader && originalGetHeader("Vary");
|
|
361
|
-
try { originalSetHeader("Vary", _appendVary(existing, "Accept-Encoding")); } catch (_e) {}
|
|
376
|
+
try { originalSetHeader("Vary", _appendVary(existing, "Accept-Encoding")); } catch (_e) { /* headers already sent */ }
|
|
362
377
|
}
|
|
363
378
|
}
|
|
364
379
|
}
|
|
@@ -383,7 +398,7 @@ function create(opts) {
|
|
|
383
398
|
// res.writeHead(). Use the implicit Content-Type from setHeader
|
|
384
399
|
// (or absence) to decide. Default Node behavior triggers
|
|
385
400
|
// writeHead with statusCode=200 here; we mirror that.
|
|
386
|
-
_decide(
|
|
401
|
+
_decide(HTTP_STATUS.OK, null);
|
|
387
402
|
if (compress) _applyCompressedHeaders(null);
|
|
388
403
|
}
|
|
389
404
|
if (!compress) return originalWrite.call(res, chunk, encArg, cbArg);
|
|
@@ -403,7 +418,7 @@ function create(opts) {
|
|
|
403
418
|
|
|
404
419
|
res.end = function (chunk, encArg, cbArg) {
|
|
405
420
|
if (!decided) {
|
|
406
|
-
_decide(
|
|
421
|
+
_decide(HTTP_STATUS.OK, null);
|
|
407
422
|
if (compress) _applyCompressedHeaders(null);
|
|
408
423
|
}
|
|
409
424
|
if (!compress) return originalEnd.call(res, chunk, encArg, cbArg);
|
package/lib/middleware/cors.js
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* opts.origins entries must be strings or RegExp. Bad config surfaces at
|
|
50
50
|
* create() not at first cross-origin request.
|
|
51
51
|
*/
|
|
52
|
+
var C = require("../constants");
|
|
52
53
|
var lazyRequire = require("../lazy-require");
|
|
53
54
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
54
55
|
var requestHelpers = require("../request-helpers");
|
|
@@ -221,7 +222,7 @@ function create(opts) {
|
|
|
221
222
|
var allowedHeadersSet = headersList.map(function (h) { return String(h).trim().toLowerCase(); });
|
|
222
223
|
var exposeHeaders = (opts.exposeHeaders || ["X-Request-Id"]).join(", ");
|
|
223
224
|
var credentials = !!opts.credentials;
|
|
224
|
-
var maxAge = String(opts.maxAgeSeconds ||
|
|
225
|
+
var maxAge = String(opts.maxAgeSeconds || (C.TIME.minutes(10) / C.TIME.seconds(1)));
|
|
225
226
|
var refuseUnknown = opts.refuseUnknown !== false;
|
|
226
227
|
// strictNullOrigin defaults true: refuse Origin: null even with
|
|
227
228
|
// Sec-Fetch-Site: same-origin (non-browser callers can forge that
|
|
@@ -250,9 +251,9 @@ function create(opts) {
|
|
|
250
251
|
metadata: { origin: origin, method: req.method, path: req.pathname || req.url, requestId: req.requestId },
|
|
251
252
|
requestId: req.requestId,
|
|
252
253
|
});
|
|
253
|
-
} catch (_e) {}
|
|
254
|
+
} catch (_e) { /* audit best-effort */ }
|
|
254
255
|
if (typeof res.writeHead === "function") {
|
|
255
|
-
res.writeHead(
|
|
256
|
+
res.writeHead(requestHelpers.HTTP_STATUS.FORBIDDEN, { "Content-Type": "text/plain" });
|
|
256
257
|
res.end("CORS: origin not allowed");
|
|
257
258
|
return;
|
|
258
259
|
}
|
|
@@ -283,7 +284,7 @@ function create(opts) {
|
|
|
283
284
|
for (var ah = 0; ah < asked.length; ah++) {
|
|
284
285
|
if (allowedHeadersSet.indexOf(asked[ah]) === -1) {
|
|
285
286
|
if (typeof res.writeHead === "function") {
|
|
286
|
-
res.writeHead(
|
|
287
|
+
res.writeHead(requestHelpers.HTTP_STATUS.FORBIDDEN, { "Content-Type": "text/plain" });
|
|
287
288
|
res.end("CORS: requested header '" + asked[ah] + "' not in allow-list");
|
|
288
289
|
}
|
|
289
290
|
return;
|
|
@@ -297,7 +298,7 @@ function create(opts) {
|
|
|
297
298
|
res.setHeader("Access-Control-Max-Age", maxAge);
|
|
298
299
|
}
|
|
299
300
|
if (typeof res.writeHead === "function") {
|
|
300
|
-
res.writeHead(
|
|
301
|
+
res.writeHead(requestHelpers.HTTP_STATUS.NO_CONTENT);
|
|
301
302
|
res.end();
|
|
302
303
|
}
|
|
303
304
|
return;
|
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
* pass `always: true`.
|
|
113
113
|
*/
|
|
114
114
|
|
|
115
|
+
var C = require("../constants");
|
|
115
116
|
var crypto = require("../crypto");
|
|
116
117
|
var numericBounds = require("../numeric-bounds");
|
|
117
118
|
var validateOpts = require("../validate-opts");
|
|
@@ -120,13 +121,13 @@ var { defineClass } = require("../framework-error");
|
|
|
120
121
|
var CspNonceError = defineClass("CspNonceError", { alwaysPermanent: true });
|
|
121
122
|
|
|
122
123
|
var DEFAULT_DIRECTIVES = Object.freeze(["script-src", "style-src"]);
|
|
123
|
-
var DEFAULT_NONCE_BYTES = 16;
|
|
124
|
-
var MIN_NONCE_BYTES = 16;
|
|
124
|
+
var DEFAULT_NONCE_BYTES = C.BYTES.bytes(16);
|
|
125
|
+
var MIN_NONCE_BYTES = C.BYTES.bytes(16);
|
|
125
126
|
var PLACEHOLDER_PREFIX = "__BLAMEJS_CSP_NONCE_";
|
|
126
127
|
var PLACEHOLDER_SUFFIX = "__";
|
|
127
128
|
// 16 bytes / 128 bits — matches the default nonce strength so the
|
|
128
129
|
// placeholder is no weaker than the value it stands in for.
|
|
129
|
-
var PLACEHOLDER_RAND_BYTES = 16;
|
|
130
|
+
var PLACEHOLDER_RAND_BYTES = C.BYTES.bytes(16);
|
|
130
131
|
|
|
131
132
|
// Build the substitute helper for a specific placeholder + property.
|
|
132
133
|
// Each cspNonce instance has its own placeholder (random by default,
|
|
@@ -158,7 +158,7 @@ function _appendSetCookie(res, value) {
|
|
|
158
158
|
function _writeReject(res, message) {
|
|
159
159
|
if (typeof res.writeHead === "function") {
|
|
160
160
|
var body = JSON.stringify({ error: message });
|
|
161
|
-
res.writeHead(
|
|
161
|
+
res.writeHead(requestHelpers.HTTP_STATUS.FORBIDDEN, {
|
|
162
162
|
"Content-Type": "application/json; charset=utf-8",
|
|
163
163
|
"Content-Length": Buffer.byteLength(body),
|
|
164
164
|
});
|
|
@@ -90,10 +90,7 @@ var ALLOWED_OPTS = [
|
|
|
90
90
|
"audit", "auditFailures", "auditSuccess",
|
|
91
91
|
];
|
|
92
92
|
|
|
93
|
-
function _emitEvent(
|
|
94
|
-
try { observability().event(name, value, labels || {}); }
|
|
95
|
-
catch (_e) { /* hot-path observability sink — drop silent by design */ }
|
|
96
|
-
}
|
|
93
|
+
function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
|
|
97
94
|
|
|
98
95
|
function _validateRoleIdentifier(role, where) {
|
|
99
96
|
try {
|
|
@@ -114,14 +111,8 @@ function create(opts) {
|
|
|
114
111
|
opts = opts || {};
|
|
115
112
|
validateOpts(opts, ALLOWED_OPTS, "middleware.dbRoleFor");
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"middleware.dbRoleFor: resolve must be a function");
|
|
120
|
-
}
|
|
121
|
-
if (opts.responder !== undefined && typeof opts.responder !== "function") {
|
|
122
|
-
throw _err("db-role-for/bad-opt",
|
|
123
|
-
"middleware.dbRoleFor: responder must be a function");
|
|
124
|
-
}
|
|
114
|
+
validateOpts.optionalFunction(opts.resolve, "middleware.dbRoleFor: resolve", DbRoleForError, "db-role-for/bad-opt");
|
|
115
|
+
validateOpts.optionalFunction(opts.responder, "middleware.dbRoleFor: responder", DbRoleForError, "db-role-for/bad-opt");
|
|
125
116
|
if (opts.permissions !== undefined && opts.permissions !== null) {
|
|
126
117
|
if (typeof opts.permissions !== "object" ||
|
|
127
118
|
typeof opts.permissions.dbRoleFor !== "function") {
|
|
@@ -137,10 +128,7 @@ function create(opts) {
|
|
|
137
128
|
}
|
|
138
129
|
_validateRoleIdentifier(opts.defaultRole, "middleware.dbRoleFor: defaultRole");
|
|
139
130
|
}
|
|
140
|
-
|
|
141
|
-
throw _err("db-role-for/bad-opt",
|
|
142
|
-
"middleware.dbRoleFor: requireRole must be a boolean");
|
|
143
|
-
}
|
|
131
|
+
validateOpts.optionalBoolean(opts.requireRole, "middleware.dbRoleFor: requireRole", DbRoleForError, "db-role-for/bad-opt");
|
|
144
132
|
if (opts.missingRoleStatus !== undefined) {
|
|
145
133
|
if (typeof opts.missingRoleStatus !== "number" ||
|
|
146
134
|
!isFinite(opts.missingRoleStatus) ||
|
|
@@ -149,20 +137,9 @@ function create(opts) {
|
|
|
149
137
|
"middleware.dbRoleFor: missingRoleStatus must be an HTTP status code (100-599)");
|
|
150
138
|
}
|
|
151
139
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"middleware.dbRoleFor: audit must be a b.audit-shaped object (safeEmit fn)");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
|
|
159
|
-
throw _err("db-role-for/bad-opt",
|
|
160
|
-
"middleware.dbRoleFor: auditFailures must be a boolean");
|
|
161
|
-
}
|
|
162
|
-
if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
|
|
163
|
-
throw _err("db-role-for/bad-opt",
|
|
164
|
-
"middleware.dbRoleFor: auditSuccess must be a boolean");
|
|
165
|
-
}
|
|
140
|
+
validateOpts.auditShape(opts.audit, "middleware.dbRoleFor", DbRoleForError, "db-role-for/bad-opt");
|
|
141
|
+
validateOpts.optionalBoolean(opts.auditFailures, "middleware.dbRoleFor: auditFailures", DbRoleForError, "db-role-for/bad-opt");
|
|
142
|
+
validateOpts.optionalBoolean(opts.auditSuccess, "middleware.dbRoleFor: auditSuccess", DbRoleForError, "db-role-for/bad-opt");
|
|
166
143
|
|
|
167
144
|
var resolveFn = opts.resolve || null;
|
|
168
145
|
var perms = opts.permissions || null;
|