@blamejs/core 0.18.39 → 0.18.41
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 +118 -0
- package/NOTICE +2 -2
- package/README.md +2 -2
- package/lib/acme.js +5 -7
- package/lib/app.js +14 -5
- package/lib/audit.js +4 -4
- package/lib/auth/dpop.js +6 -6
- package/lib/cert.js +12 -12
- package/lib/compliance-sanctions.js +4 -4
- package/lib/cookies.js +98 -14
- package/lib/file-upload.js +17 -3
- package/lib/gate-contract.js +159 -5
- package/lib/guard-filename.js +93 -18
- package/lib/guard-yaml.js +64 -0
- package/lib/http-client.js +1 -3
- package/lib/mail-auth.js +477 -78
- package/lib/mail.js +1 -2
- package/lib/middleware/csrf-protect.js +45 -51
- package/lib/migrations.js +22 -22
- package/lib/network-dns-resolver.js +23 -23
- package/lib/network-dns.js +205 -43
- package/lib/public-suffix.js +110 -24
- package/lib/seeders.js +15 -15
- package/lib/session.js +99 -8
- package/lib/vendor/MANIFEST.json +14 -14
- package/lib/vendor/blamejs-pki.cjs +1385 -402
- package/lib/vendor/public-suffix-list.dat +4 -3
- package/lib/vendor/public-suffix-list.data.js +2201 -2201
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/gate-contract.js
CHANGED
|
@@ -85,7 +85,9 @@ var DEFAULT_CACHE_TTL_MS = C.TIME.minutes(5);
|
|
|
85
85
|
* shape violations: `gate-contract/bad-shape` from
|
|
86
86
|
* `validateGateShape`, `gate-contract/bad-opt` from `defineGate` /
|
|
87
87
|
* `cachingGate` / `workerThreadGate`, `gate-contract/profile-cycle`
|
|
88
|
-
* and `gate-contract/unknown-profile` from `buildProfile
|
|
88
|
+
* and `gate-contract/unknown-profile` from `buildProfile`, and
|
|
89
|
+
* `gate-contract/bad-context` from a gate's `check` when the context
|
|
90
|
+
* it is handed is neither an object nor absent.
|
|
89
91
|
* `alwaysPermanent` — never retried by `b.retry`.
|
|
90
92
|
*
|
|
91
93
|
* @example
|
|
@@ -290,6 +292,126 @@ function validateGateShape(gate, label, errorClass) {
|
|
|
290
292
|
* var d = await gate.check({ bytes: Buffer.from("name,age\nada,36") });
|
|
291
293
|
* d.action; // → "serve"
|
|
292
294
|
*/
|
|
295
|
+
// A gate context with some fields REPLACED, leaving the caller's object alone.
|
|
296
|
+
// Used by the two paths that genuinely change the context — a `beforeCheck`
|
|
297
|
+
// transform, and the sanitize rebind that feeds scrubbed bytes to the next gate
|
|
298
|
+
// in a composition. Module scope on purpose: `composeGates` is not inside
|
|
299
|
+
// defineGate's closure.
|
|
300
|
+
//
|
|
301
|
+
// A context is not required to be a plain object, and the obvious shortcuts
|
|
302
|
+
// each break a different kind of one. Every line here is a fix for a shape that
|
|
303
|
+
// was mishandled:
|
|
304
|
+
//
|
|
305
|
+
// - `Object.assign({}, base, ...)` copies OWN enumerable properties, so a
|
|
306
|
+
// class instance whose fields come from prototype getters arrives with them
|
|
307
|
+
// missing. A guard reading an absent subject as nothing-to-inspect then
|
|
308
|
+
// serves bytes it used to examine.
|
|
309
|
+
// - `Object.create(base)` fixes that but makes inherited getters run with the
|
|
310
|
+
// DERIVED object as `this`, so a getter returning a private field throws
|
|
311
|
+
// and a valid context is refused.
|
|
312
|
+
// - Assigning the overrides (including via `Object.assign`) performs [[Set]],
|
|
313
|
+
// which walks the chain: an inherited non-writable property throws, and an
|
|
314
|
+
// inherited setter runs and writes into the caller.
|
|
315
|
+
//
|
|
316
|
+
// So: forward each readable name to `base` with `base` as the receiver, and
|
|
317
|
+
// install the overrides as own data properties via [[DefineOwnProperty]]. The
|
|
318
|
+
// readable names are collected once, which is sound because both callers derive
|
|
319
|
+
// from a context that is already fully formed.
|
|
320
|
+
function _deriveContext(base, overrides) {
|
|
321
|
+
// Same prototype as the caller's object, so `ctx instanceof TheirClass` still
|
|
322
|
+
// answers what it did — a guard may dispatch or validate on the context's
|
|
323
|
+
// type, and a wrapper on a bare object would fail that while carrying every
|
|
324
|
+
// one of the instance's fields. The forwarding accessors below are installed
|
|
325
|
+
// as OWN properties, so they shadow the prototype for everything readable and
|
|
326
|
+
// the prototype serves only identity.
|
|
327
|
+
var derived = Object.create(Object.getPrototypeOf(base));
|
|
328
|
+
var seen = Object.create(null);
|
|
329
|
+
// Copy-on-write. The getter forwards to `base` with `base` as the receiver;
|
|
330
|
+
// the setter REPLACES itself with a plain own data property holding the new
|
|
331
|
+
// value, so a guard that normalizes its subject in place (`ctx.bytes = ...`)
|
|
332
|
+
// succeeds and every later read sees the update. Without the setter the
|
|
333
|
+
// assignment silently does nothing, or throws under strict mode and turns a
|
|
334
|
+
// completed check into a refusal. The caller's object is still never touched.
|
|
335
|
+
//
|
|
336
|
+
// Functions are forwarded AS THEY ARE — never bound. That is a deliberate
|
|
337
|
+
// choice between two things that cannot both hold, and it is worth stating
|
|
338
|
+
// because the losing side is a real capability:
|
|
339
|
+
//
|
|
340
|
+
// - Called as `ctx.read()`, an unbound method runs with the DERIVED context
|
|
341
|
+
// as `this`, so a method reading `this.bytes` sees an override a
|
|
342
|
+
// `beforeCheck` transform or a sanitize step installed. This is the whole
|
|
343
|
+
// point of a sanitize chain: nothing downstream may see the original
|
|
344
|
+
// bytes. Binding to the caller's object was measured handing back
|
|
345
|
+
// `ORIGINAL` from a method while direct access returned `SANITIZED`.
|
|
346
|
+
// - Bound to the caller's object instead, a method carrying a brand — one
|
|
347
|
+
// reading a private field, or a built-in like `Map#get` — works, but then
|
|
348
|
+
// every ordinary method reads around the overrides.
|
|
349
|
+
//
|
|
350
|
+
// A brand requires the original instance as the receiver; an override
|
|
351
|
+
// requires the derived one. No receiver satisfies both, so the security
|
|
352
|
+
// property wins: overrides are always visible. A context whose data is only
|
|
353
|
+
// reachable through a branded method is therefore REFUSED rather than
|
|
354
|
+
// inspected against stale bytes — fail-closed, and visible to the operator,
|
|
355
|
+
// who can pass the fields directly instead.
|
|
356
|
+
//
|
|
357
|
+
// Not binding also keeps every function's identity and lets a guard choose a
|
|
358
|
+
// receiver with `.call()`, which matters for a callback the caller stored on
|
|
359
|
+
// the context.
|
|
360
|
+
var forward = function (key, enumerable) {
|
|
361
|
+
Object.defineProperty(derived, key, {
|
|
362
|
+
get: function () { return base[key]; },
|
|
363
|
+
set: function (value) {
|
|
364
|
+
Object.defineProperty(derived, key, {
|
|
365
|
+
value: value,
|
|
366
|
+
writable: true,
|
|
367
|
+
enumerable: enumerable,
|
|
368
|
+
configurable: true,
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
// Mirror the source. Enumerability is observable — a guard that spreads
|
|
372
|
+
// the context, calls Object.keys, or serializes it would otherwise see
|
|
373
|
+
// fields the original hid, and serializing a hidden accessor RUNS a
|
|
374
|
+
// getter the guard never chose to read.
|
|
375
|
+
enumerable: enumerable,
|
|
376
|
+
configurable: true,
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
// Reflect.ownKeys, not getOwnPropertyNames: a guard may tag its context with
|
|
380
|
+
// a Symbol to avoid colliding with a caller's field names, and a
|
|
381
|
+
// string-key-only walk drops it silently.
|
|
382
|
+
for (var proto = base; proto && proto !== Object.prototype;
|
|
383
|
+
proto = Object.getPrototypeOf(proto)) {
|
|
384
|
+
var names = Reflect.ownKeys(proto);
|
|
385
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
386
|
+
var key = names[i];
|
|
387
|
+
if (seen[key]) continue;
|
|
388
|
+
var desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
389
|
+
// Skip a class prototype's own back-reference to its constructor, which
|
|
390
|
+
// is plumbing rather than context — but NOT a field the caller happens to
|
|
391
|
+
// have named `constructor`, which is theirs and carries their value.
|
|
392
|
+
if (key === "constructor" && proto !== base && desc &&
|
|
393
|
+
typeof desc.value === "function" && desc.value.prototype === proto) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
seen[key] = true;
|
|
397
|
+
forward(key, desc ? desc.enumerable : true);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (overrides) {
|
|
401
|
+
var keys = Reflect.ownKeys(overrides);
|
|
402
|
+
for (var k = 0; k < keys.length; k += 1) {
|
|
403
|
+
var od = Object.getOwnPropertyDescriptor(overrides, keys[k]);
|
|
404
|
+
Object.defineProperty(derived, keys[k], {
|
|
405
|
+
value: overrides[keys[k]],
|
|
406
|
+
writable: true,
|
|
407
|
+
enumerable: od ? od.enumerable : true,
|
|
408
|
+
configurable: true,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return derived;
|
|
413
|
+
}
|
|
414
|
+
|
|
293
415
|
function defineGate(opts) {
|
|
294
416
|
validateOpts.requireObject(opts, "gateContract.defineGate", GateContractError);
|
|
295
417
|
validateOpts.requireNonEmptyString(opts.name, "gateContract.defineGate: name", GateContractError, "gate-contract/bad-opt");
|
|
@@ -350,8 +472,30 @@ function defineGate(opts) {
|
|
|
350
472
|
|
|
351
473
|
async function check(ctx) {
|
|
352
474
|
var startedAt = Date.now();
|
|
353
|
-
|
|
354
|
-
|
|
475
|
+
// The gate owns the context it works with; the caller's object is read, not
|
|
476
|
+
// written. Stamping the forensic id onto whatever arrived meant a string
|
|
477
|
+
// raised a raw TypeError — and `check` is async, so that surfaced as an
|
|
478
|
+
// unhandled rejection taking the process down rather than the request — and
|
|
479
|
+
// a frozen context, which an operator freezes precisely so middleware
|
|
480
|
+
// cannot edit the request shape, was refused for being frozen.
|
|
481
|
+
if (ctx === null || ctx === undefined) {
|
|
482
|
+
ctx = {};
|
|
483
|
+
} else if (typeof ctx !== "object") {
|
|
484
|
+
throw _err("gate-contract/bad-context",
|
|
485
|
+
opts.name + ".check: context must be an object (got " + typeof ctx + ")");
|
|
486
|
+
}
|
|
487
|
+
// A guard can read `ctx.forensicId` to correlate its own audit records, so
|
|
488
|
+
// the id stays visible on the context the check receives. What changes is
|
|
489
|
+
// how it gets there: an id the caller supplied is used as-is and the
|
|
490
|
+
// caller's object is passed straight through, and only when one has to be
|
|
491
|
+
// generated is a context derived to carry it — never by writing into the
|
|
492
|
+
// object the caller handed over.
|
|
493
|
+
// Derive unconditionally. Whether the caller supplied an id decides only
|
|
494
|
+
// whether one is generated — never whether the caller's object is isolated,
|
|
495
|
+
// because a hook or guard that assigns to the context would otherwise be
|
|
496
|
+
// writing into the caller's own state, and would throw against a frozen one.
|
|
497
|
+
var forensicId = ctx.forensicId || bCrypto.generateToken(FORENSIC_ID_BYTES);
|
|
498
|
+
ctx = _deriveContext(ctx, { forensicId: forensicId });
|
|
355
499
|
|
|
356
500
|
// Decision cache lookup (memoize per-forensicHash).
|
|
357
501
|
var bytes = ctx.bytes;
|
|
@@ -376,7 +520,11 @@ function defineGate(opts) {
|
|
|
376
520
|
return _build({ ok: true, action: "serve", forensicHash: forensicHash, runtimeMs: Date.now() - startedAt });
|
|
377
521
|
}
|
|
378
522
|
if (beforeRv && beforeRv.transform) {
|
|
379
|
-
|
|
523
|
+
// Extend the chain, never flatten it: the caller's fields are reached
|
|
524
|
+
// THROUGH `ctx` rather than owned by it, so rebuilding from own
|
|
525
|
+
// enumerable properties would drop the subject on exactly the gates an
|
|
526
|
+
// operator attached a hook to.
|
|
527
|
+
ctx = _deriveContext(ctx, beforeRv.transform);
|
|
380
528
|
}
|
|
381
529
|
|
|
382
530
|
// Run operator check with optional runtime cap.
|
|
@@ -455,6 +603,10 @@ function defineGate(opts) {
|
|
|
455
603
|
decision.forensicSnapshot = snippet;
|
|
456
604
|
if (forensicEvidenceStore && typeof forensicEvidenceStore.write === "function") {
|
|
457
605
|
await forensicEvidenceStore.write({
|
|
606
|
+
// Read from the context as it stands, not from the id computed
|
|
607
|
+
// before the hooks ran: a `beforeCheck` transform may replace the
|
|
608
|
+
// forensic id, and the guard saw the replacement. Recording the
|
|
609
|
+
// earlier one breaks exactly the correlation the id exists for.
|
|
458
610
|
forensicId: ctx.forensicId,
|
|
459
611
|
forensicHash: forensicHash,
|
|
460
612
|
ruleHash: ruleHash,
|
|
@@ -664,7 +816,9 @@ function composeGates(gates, opts) {
|
|
|
664
816
|
if (d.sanitized) sanitized = d.sanitized;
|
|
665
817
|
// Feeding the scrubbed bytes forward is what makes the chain a
|
|
666
818
|
// pipeline rather than N independent opinions on the same input.
|
|
667
|
-
|
|
819
|
+
// Same chain-extending rebind as the beforeCheck transform above —
|
|
820
|
+
// only `bytes` is being replaced, not the rest of the context.
|
|
821
|
+
if (firstRefusalWins) ctx = _deriveContext(ctx, { bytes: d.sanitized });
|
|
668
822
|
}
|
|
669
823
|
}
|
|
670
824
|
return _build({
|
package/lib/guard-filename.js
CHANGED
|
@@ -881,7 +881,17 @@ function sanitize(input, opts) {
|
|
|
881
881
|
* every reject-policy off — strip-eligible classes only) → `refuse`
|
|
882
882
|
* (any reject-policy active or sanitize fails). Path-traversal /
|
|
883
883
|
* null-byte / NTFS-ADS / UNC / overlong-UTF-8 always cause `refuse`
|
|
884
|
-
* — there is no `sanitize` action for those classes
|
|
884
|
+
* — there is no `sanitize` action for those classes, and no policy
|
|
885
|
+
* setting reaches them.
|
|
886
|
+
*
|
|
887
|
+
* Each finding is dispositioned by its own policy and the strongest
|
|
888
|
+
* answer across them decides the action. The repair itself is not
|
|
889
|
+
* per-finding: `sanitize` dispatches to `b.guardFilename.sanitize`,
|
|
890
|
+
* which applies every transform the profile declares, so a name that
|
|
891
|
+
* enters sanitization because one class asked to strip also has the
|
|
892
|
+
* profile's other repairs applied — including to a class whose own
|
|
893
|
+
* policy was `audit`. The verdict's `sanitized` is byte-identical to
|
|
894
|
+
* calling `b.guardFilename.sanitize(name, opts)` directly.
|
|
885
895
|
*
|
|
886
896
|
* @opts
|
|
887
897
|
* profile: "strict"|"balanced"|"permissive",
|
|
@@ -896,6 +906,52 @@ function sanitize(input, opts) {
|
|
|
896
906
|
* var ok = await fnGate.check({ filename: "report.txt" });
|
|
897
907
|
* ok.action; // → "serve"
|
|
898
908
|
*/
|
|
909
|
+
// Bind each finding to the operator's policy for it. The shared character
|
|
910
|
+
// classes resolve through the one family helper; the filename-specific kinds
|
|
911
|
+
// map to the policy this guard names for them. A kind with no policy returns
|
|
912
|
+
// null and the caller applies the conservative severity answer.
|
|
913
|
+
// The classes this guard refuses unconditionally, whatever any policy says.
|
|
914
|
+
// Documented on the gate itself: there is no `sanitize` action for them, and a
|
|
915
|
+
// name carrying one is not repairable into a safe name — a UNC prefix reaches
|
|
916
|
+
// another host, a traversal segment escapes the directory, a NUL truncates the
|
|
917
|
+
// name at whichever consumer reads it first, and an ADS suffix names a second
|
|
918
|
+
// stream on the same file. Reading these from `traversalPolicy` would let a
|
|
919
|
+
// profile that sets it to `audit` serve the name unchanged, which is the
|
|
920
|
+
// bypass the floor exists to prevent.
|
|
921
|
+
var ALWAYS_REFUSE_KINDS = Object.freeze({
|
|
922
|
+
"path-traversal": true,
|
|
923
|
+
"path-traversal-encoded": true,
|
|
924
|
+
"unc-path": true,
|
|
925
|
+
"ntfs-ads": true,
|
|
926
|
+
"null-byte": true,
|
|
927
|
+
"overlong-utf8": true,
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
function _gateDispositionFor(issue, opts) {
|
|
931
|
+
if (ALWAYS_REFUSE_KINDS[issue.kind]) return "refuse";
|
|
932
|
+
var shared = gateContract.charThreatDisposition(issue, opts);
|
|
933
|
+
if (shared) return shared;
|
|
934
|
+
switch (issue.kind) {
|
|
935
|
+
case "path-separator-in-leaf":
|
|
936
|
+
case "url-encoded-separator": return gateContract.policyDisposition(opts.pathSeparatorsPolicy);
|
|
937
|
+
case "reserved-char": return gateContract.policyDisposition(opts.reservedCharPolicy);
|
|
938
|
+
case "reserved-name": return gateContract.policyDisposition(opts.reservedNamePolicy);
|
|
939
|
+
case "leading-trailing-strip": return gateContract.policyDisposition(opts.leadingTrailingPolicy);
|
|
940
|
+
case "homoglyph": return gateContract.policyDisposition(opts.homoglyphPolicy);
|
|
941
|
+
case "non-ascii": return gateContract.policyDisposition(opts.nonAsciiPolicy);
|
|
942
|
+
// Both of these fire on the same condition — a last extension in
|
|
943
|
+
// SHELL_EXEC_EXTS — so they are one finding reported twice and answer to
|
|
944
|
+
// one policy. Mapping only the first left the second on the conservative
|
|
945
|
+
// severity default, where `critical` refuses, and a profile asking to
|
|
946
|
+
// audit a disguised executable refused it instead.
|
|
947
|
+
case "shell-exec-ext":
|
|
948
|
+
case "double-extension": return gateContract.policyDisposition(opts.shellExecExtPolicy);
|
|
949
|
+
// Length, extension allowlisting and the dot-shape findings carry no
|
|
950
|
+
// policy of their own and admit no repair that preserves intent.
|
|
951
|
+
default: return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
899
955
|
function gate(opts) {
|
|
900
956
|
opts = _resolveOpts(opts);
|
|
901
957
|
return gateContract.buildGuardGate(
|
|
@@ -907,27 +963,46 @@ function gate(opts) {
|
|
|
907
963
|
if (!name) return { ok: true, action: "serve" };
|
|
908
964
|
var rv = validate(name, opts);
|
|
909
965
|
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
966
|
+
|
|
967
|
+
// The action comes from what each finding's OWN policy asks, and the
|
|
968
|
+
// strongest answer across the findings wins. Resolving it from severity
|
|
969
|
+
// instead made `critical` and `high` both refuse, so a profile asking to
|
|
970
|
+
// strip a zero-width character refused the filename rather than cleaning
|
|
971
|
+
// it — and the sanitize-eligibility test below compounded that by being
|
|
972
|
+
// all-or-nothing: one `reject` policy anywhere in the profile made every
|
|
973
|
+
// OTHER policy refuse too, whatever it declared.
|
|
974
|
+
//
|
|
975
|
+
// Taking the strongest disposition keeps that from weakening anything: a
|
|
976
|
+
// traversal finding still refuses on its own policy no matter what the
|
|
977
|
+
// character policies say, which is the case the all-or-nothing test was
|
|
978
|
+
// reaching for.
|
|
979
|
+
var strongest = "serve";
|
|
980
|
+
var RANK = { serve: 0, "audit-only": 1, sanitize: 2, refuse: 3 };
|
|
981
|
+
for (var qi = 0; qi < rv.issues.length; qi += 1) {
|
|
982
|
+
var d = _gateDispositionFor(rv.issues[qi], opts);
|
|
983
|
+
if (d === "audit") d = "audit-only";
|
|
984
|
+
// A finding with no policy of its own carries no instruction, so it
|
|
985
|
+
// falls back to the conservative severity answer rather than serving.
|
|
986
|
+
if (!d) {
|
|
987
|
+
d = (rv.issues[qi].severity === "critical" || rv.issues[qi].severity === "high")
|
|
988
|
+
? "refuse" : "audit-only";
|
|
989
|
+
}
|
|
990
|
+
if (RANK[d] > RANK[strongest]) strongest = d;
|
|
991
|
+
}
|
|
992
|
+
if (strongest === "serve") return { ok: true, action: "serve", issues: rv.issues };
|
|
993
|
+
if (strongest === "audit-only") return { ok: true, action: "audit-only", issues: rv.issues };
|
|
994
|
+
|
|
995
|
+
if (strongest === "sanitize") {
|
|
926
996
|
try {
|
|
927
997
|
var clean = sanitize(name, opts);
|
|
998
|
+
// `sanitized` is the field the gate contract carries through; a
|
|
999
|
+
// guard-specific name is dropped by the verdict builder, which would
|
|
1000
|
+
// hand the caller `action: "sanitize"` with nothing to use. This
|
|
1001
|
+
// branch was all but unreachable while sanitize-eligibility was
|
|
1002
|
+
// all-or-nothing, so the wrong field name never showed.
|
|
928
1003
|
return {
|
|
929
1004
|
ok: true, action: "sanitize",
|
|
930
|
-
|
|
1005
|
+
sanitized: clean,
|
|
931
1006
|
issues: rv.issues,
|
|
932
1007
|
};
|
|
933
1008
|
} catch (_e) { /* fall through */ }
|
package/lib/guard-yaml.js
CHANGED
|
@@ -735,6 +735,69 @@ function _sanitizeTransform(input, opts) {
|
|
|
735
735
|
return codepointClass.scrubCharThreats(input, opts, _err, "yaml");
|
|
736
736
|
}
|
|
737
737
|
|
|
738
|
+
/**
|
|
739
|
+
* @primitive b.guardYaml.gate
|
|
740
|
+
* @signature b.guardYaml.gate(opts?)
|
|
741
|
+
* @since 0.7.14
|
|
742
|
+
* @status stable
|
|
743
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
744
|
+
* @related b.guardYaml.validate, b.guardYaml.parse, b.staticServe.create, b.fileUpload.create
|
|
745
|
+
*
|
|
746
|
+
* Build a `b.gateContract` gate for plugging into
|
|
747
|
+
* `b.staticServe({ contentSafety: { ".yaml": gate } })`,
|
|
748
|
+
* `b.fileUpload({ contentSafety: { "application/yaml": gate } })`,
|
|
749
|
+
* or any host primitive that consumes the gate-contract shape.
|
|
750
|
+
*
|
|
751
|
+
* The action comes from the POLICY the active profile declares for each
|
|
752
|
+
* finding, not from the finding's severity: a character class set to `strip`
|
|
753
|
+
* is repaired and returned as `sanitize`, one set to `audit` reports
|
|
754
|
+
* `audit-only`, and one set to `reject` refuses. Findings that carry no policy
|
|
755
|
+
* and admit no repair — an alias explosion, a blown anchor cap, an oversized
|
|
756
|
+
* document, one that does not parse — fall back to the conservative severity
|
|
757
|
+
* answer and refuse.
|
|
758
|
+
*
|
|
759
|
+
* Only character-level repair is offered. Stripping an invisible or control
|
|
760
|
+
* character is a text edit needing no re-emit; the tag, alias and
|
|
761
|
+
* multi-document shapes have no faithful round-trip, so they refuse through
|
|
762
|
+
* their own policies rather than being rewritten.
|
|
763
|
+
*
|
|
764
|
+
* @opts
|
|
765
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
766
|
+
* compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
767
|
+
* name: string, // gate identity for audit / observability
|
|
768
|
+
*
|
|
769
|
+
* @example
|
|
770
|
+
* var yamlGate = b.guardYaml.gate({ profile: "balanced" });
|
|
771
|
+
* var d = await yamlGate.check({ bytes: Buffer.from("key: value\n") });
|
|
772
|
+
* d.action; // → "serve"
|
|
773
|
+
*/
|
|
774
|
+
// The gate every other content guard builds explicitly. Without it `defineGuard`
|
|
775
|
+
// falls back to the default gate, which ends in `severityDisposition` and never
|
|
776
|
+
// consults this guard's disposition map — so a class the profile sets to `strip`
|
|
777
|
+
// or `audit` was refused anyway, because its finding happens to carry a high
|
|
778
|
+
// severity. Seven of the twelve character-policy cells resolved that way.
|
|
779
|
+
function gate(opts) {
|
|
780
|
+
// Resolve the profile FIRST. `buildContentGate` hands these opts straight to
|
|
781
|
+
// the disposition map, and an unresolved bag carries no `zeroWidthPolicy` /
|
|
782
|
+
// `bidiPolicy` / `controlPolicy` / `nullBytePolicy` at all — so every char
|
|
783
|
+
// finding falls through to the severity answer and refuses, which is the
|
|
784
|
+
// same wrong outcome by a different route.
|
|
785
|
+
opts = gateContract.resolveProfileAndPosture(opts, {
|
|
786
|
+
profiles: PROFILES,
|
|
787
|
+
compliancePostures: COMPLIANCE_POSTURES,
|
|
788
|
+
defaults: DEFAULTS,
|
|
789
|
+
errorClass: GuardYamlError,
|
|
790
|
+
errCodePrefix: "yaml",
|
|
791
|
+
});
|
|
792
|
+
return gateContract.buildContentGate({
|
|
793
|
+
name: opts.name || "guardYaml:" + (opts.profile || "default"),
|
|
794
|
+
opts: opts,
|
|
795
|
+
validate: module.exports.validate,
|
|
796
|
+
dispositionFor: _gateDispositionFor,
|
|
797
|
+
produceSanitized: _sanitizeTransform,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
738
801
|
module.exports = gateContract.defineGuard({
|
|
739
802
|
name: "yaml",
|
|
740
803
|
kind: "content",
|
|
@@ -748,6 +811,7 @@ module.exports = gateContract.defineGuard({
|
|
|
748
811
|
detect: _detectIssues,
|
|
749
812
|
intOpts: ["maxBytes", "maxDepth", "maxAnchors", "maxAliasDepth",
|
|
750
813
|
"maxDocuments", "maxNodes", "maxScalarLength"],
|
|
814
|
+
gate: gate,
|
|
751
815
|
dispositionFor: _gateDispositionFor,
|
|
752
816
|
sanitizeTransform: _sanitizeTransform,
|
|
753
817
|
extra: {
|
package/lib/http-client.js
CHANGED
|
@@ -851,9 +851,7 @@ function _attachJarCookie(headers, jar, url) {
|
|
|
851
851
|
function _buildMultipartBody(spec) {
|
|
852
852
|
var boundary = "----blamejs-mp-" + bCrypto.generateToken(C.BYTES.bytes(16));
|
|
853
853
|
var CRLF = "\r\n";
|
|
854
|
-
var
|
|
855
|
-
var path = require("node:path"); // allow:inline-require — same
|
|
856
|
-
var nodeStream = require("node:stream"); // allow:inline-require — Readable subclass only when streaming
|
|
854
|
+
var path = require("node:path"); // allow:inline-require — only on multipart paths that touch the filesystem
|
|
857
855
|
|
|
858
856
|
// Each entry is { headerBytes, source } where source is one of:
|
|
859
857
|
// { kind: "buffer", buf: Buffer }
|