@blamejs/core 0.8.90 → 0.9.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 +847 -846
- package/index.js +6 -0
- package/lib/ai-pref.js +8 -2
- package/lib/auth/step-up.js +14 -5
- package/lib/cdn-cache-control.js +473 -0
- package/lib/client-hints.js +318 -0
- package/lib/http-client-cache.js +15 -8
- package/lib/http-client-cookie-jar.js +18 -7
- package/lib/http-message-signature.js +18 -11
- package/lib/log-stream.js +25 -1
- package/lib/mail-auth.js +3 -2
- package/lib/mail-require-tls.js +7 -18
- package/lib/middleware/body-parser.js +24 -12
- package/lib/middleware/scim-server.js +2 -2
- package/lib/middleware/tus-upload.js +12 -7
- package/lib/network-dns.js +178 -0
- package/lib/network-smtp-policy.js +2 -2
- package/lib/request-helpers.js +15 -0
- package/lib/security-assert.js +23 -1
- package/lib/structured-fields.js +244 -0
- package/lib/websocket.js +15 -9
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -112,13 +112,14 @@ var fs = require("fs");
|
|
|
112
112
|
var os = require("os");
|
|
113
113
|
var path = require("path");
|
|
114
114
|
var nodeCrypto = require("node:crypto");
|
|
115
|
-
var atomicFile
|
|
116
|
-
var crypto
|
|
117
|
-
var lazyRequire
|
|
118
|
-
var requestHelpers
|
|
119
|
-
var safeBuffer
|
|
120
|
-
var safeJson
|
|
121
|
-
var
|
|
115
|
+
var atomicFile = require("../atomic-file");
|
|
116
|
+
var crypto = require("../crypto");
|
|
117
|
+
var lazyRequire = require("../lazy-require");
|
|
118
|
+
var requestHelpers = require("../request-helpers");
|
|
119
|
+
var safeBuffer = require("../safe-buffer");
|
|
120
|
+
var safeJson = require("../safe-json");
|
|
121
|
+
var structuredFields = require("../structured-fields");
|
|
122
|
+
var validateOpts = require("../validate-opts");
|
|
122
123
|
var C = require("../constants");
|
|
123
124
|
var { defineClass } = require("../framework-error");
|
|
124
125
|
|
|
@@ -214,14 +215,20 @@ function _contentType(req) {
|
|
|
214
215
|
var params = {};
|
|
215
216
|
if (idx !== -1) {
|
|
216
217
|
var rest = ct.slice(idx + 1);
|
|
217
|
-
|
|
218
|
+
// RFC 9110 §8.3 + §5.6.6 — parameter values may be quoted-string
|
|
219
|
+
// (e.g. `boundary="foo;bar"`, `charset="x;y"`). Bare `.split(";")`
|
|
220
|
+
// would slice through quoted commas/semicolons and corrupt the
|
|
221
|
+
// multipart boundary. Use the shared quote-aware splitter that
|
|
222
|
+
// tracks RFC 8941 §3.3.3 quoted-string state with backslash-escape.
|
|
223
|
+
var parts = structuredFields.splitTopLevel(rest, ";");
|
|
218
224
|
for (var i = 0; i < parts.length; i++) {
|
|
219
225
|
var p = parts[i].trim();
|
|
220
226
|
var eq = p.indexOf("=");
|
|
221
227
|
if (eq === -1) continue;
|
|
222
228
|
var k = p.slice(0, eq).trim().toLowerCase();
|
|
223
229
|
var v = p.slice(eq + 1).trim();
|
|
224
|
-
|
|
230
|
+
var _unq = structuredFields.unquoteSfString(v);
|
|
231
|
+
if (_unq !== null) v = _unq;
|
|
225
232
|
params[k] = v;
|
|
226
233
|
}
|
|
227
234
|
}
|
|
@@ -305,7 +312,7 @@ function _detectSmuggling(req) {
|
|
|
305
312
|
// (RFC 9112 §6.1). Anything else is a smuggling vector or
|
|
306
313
|
// server-side decode error.
|
|
307
314
|
if (typeof te === "string" && te.length > 0) {
|
|
308
|
-
var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); });
|
|
315
|
+
var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); }); // allow:bare-split-on-quoted-header — RFC 9112 §6.1 Transfer-Encoding values (chunked / gzip / deflate / identity) are token-only; no quoted-string in the grammar
|
|
309
316
|
var last = tokens[tokens.length - 1];
|
|
310
317
|
if (last !== "chunked") {
|
|
311
318
|
return {
|
|
@@ -621,7 +628,11 @@ function _parseHeaderParams(headerValue) {
|
|
|
621
628
|
// `filename` so downstream consumers don't need parser-aware code.
|
|
622
629
|
var out = { _value: "" };
|
|
623
630
|
if (!headerValue) return out;
|
|
624
|
-
|
|
631
|
+
// RFC 6266 §4.1 + RFC 9110 §5.6.6 — parameter values may be
|
|
632
|
+
// quoted-string (e.g. `filename="weird;name.txt"`). Bare
|
|
633
|
+
// `.split(";")` would slice through the quoted semicolon and
|
|
634
|
+
// corrupt the filename. Quote-aware shared splitter.
|
|
635
|
+
var parts = structuredFields.splitTopLevel(headerValue, ";");
|
|
625
636
|
out._value = parts[0].trim().toLowerCase();
|
|
626
637
|
var extName = null;
|
|
627
638
|
for (var i = 1; i < parts.length; i++) {
|
|
@@ -630,7 +641,8 @@ function _parseHeaderParams(headerValue) {
|
|
|
630
641
|
if (eq === -1) continue;
|
|
631
642
|
var k = p.slice(0, eq).trim().toLowerCase();
|
|
632
643
|
var v = p.slice(eq + 1).trim();
|
|
633
|
-
|
|
644
|
+
var _unq = structuredFields.unquoteSfString(v);
|
|
645
|
+
if (_unq !== null) v = _unq;
|
|
634
646
|
if (k.charAt(k.length - 1) === "*") {
|
|
635
647
|
var decoded = _decodeRfc5987(v);
|
|
636
648
|
if (decoded !== null) {
|
|
@@ -177,8 +177,8 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize) {
|
|
|
177
177
|
count: pageSize,
|
|
178
178
|
sortBy: query.sortBy || null,
|
|
179
179
|
sortOrder: query.sortOrder || null,
|
|
180
|
-
attributes: query.attributes ? query.attributes.split(",") : null,
|
|
181
|
-
excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null,
|
|
180
|
+
attributes: query.attributes ? query.attributes.split(",") : null, // allow:bare-split-on-quoted-header — RFC 7644 §3.9 attributes/excludedAttributes are SCIM attribute paths (URN-ish identifiers); grammar excludes DQUOTE
|
|
181
|
+
excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null, // allow:bare-split-on-quoted-header — same SCIM attribute-name grammar
|
|
182
182
|
}, ctx);
|
|
183
183
|
_writeJson(res, H.OK, {
|
|
184
184
|
schemas: [SCIM_MESSAGE_LIST],
|
|
@@ -40,13 +40,14 @@
|
|
|
40
40
|
* cannot satisfy.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
var nodeCrypto
|
|
44
|
-
var C
|
|
45
|
-
var bCrypto
|
|
46
|
-
var lazyRequire
|
|
47
|
-
var safeAsync
|
|
48
|
-
var safeBuffer
|
|
49
|
-
var
|
|
43
|
+
var nodeCrypto = require("crypto"); // for createHash() in checksum extension
|
|
44
|
+
var C = require("../constants");
|
|
45
|
+
var bCrypto = require("../crypto");
|
|
46
|
+
var lazyRequire = require("../lazy-require");
|
|
47
|
+
var safeAsync = require("../safe-async");
|
|
48
|
+
var safeBuffer = require("../safe-buffer");
|
|
49
|
+
var structuredFields = require("../structured-fields");
|
|
50
|
+
var validateOpts = require("../validate-opts");
|
|
50
51
|
var { defineClass } = require("../framework-error");
|
|
51
52
|
|
|
52
53
|
// Observability metric prefix for the TUS middleware. The framework
|
|
@@ -145,6 +146,10 @@ function _serializeMetadata(metaObj) {
|
|
|
145
146
|
function _parseChecksumHeader(headerValue, allowedSet) {
|
|
146
147
|
// tus.io 1.0.0 §3.5: `Upload-Checksum: <algo> <base64-digest>`.
|
|
147
148
|
if (typeof headerValue !== "string") return null;
|
|
149
|
+
// The tus.io grammar implicitly excludes C0 / DEL (token + base64
|
|
150
|
+
// alphabet); refuse those on the RAW value BEFORE the slice/trim
|
|
151
|
+
// normalisation (same v0.8.90 trim-before-validate bug class).
|
|
152
|
+
if (structuredFields.containsControlBytes(headerValue)) return { error: "malformed" };
|
|
148
153
|
var sp = headerValue.indexOf(" ");
|
|
149
154
|
if (sp === -1) return { error: "malformed" };
|
|
150
155
|
var algo = headerValue.slice(0, sp).trim().toLowerCase();
|
package/lib/network-dns.js
CHANGED
|
@@ -1708,9 +1708,187 @@ function isNullMx(mxRecords) {
|
|
|
1708
1708
|
return only.exchange === "" || only.exchange === ".";
|
|
1709
1709
|
}
|
|
1710
1710
|
|
|
1711
|
+
// RFC 9905 — Deprecating DNSSEC SHA-1 Usage. The IANA DNSSEC Algorithm
|
|
1712
|
+
// Numbers registry classifies SHA-1-based DNSKEY algorithms (5
|
|
1713
|
+
// RSASHA1, 7 RSASHA1-NSEC3-SHA1, 10 RSASHA512-using-SHA1-NSEC3) and
|
|
1714
|
+
// SHA-1 DS digest type 1 as "MUST NOT be used" / "MUST NOT be
|
|
1715
|
+
// supported". Operators auditing inbound DNSSEC chain-of-trust data
|
|
1716
|
+
// classify a record's algorithm number to decide whether to refuse
|
|
1717
|
+
// the validation as deprecated.
|
|
1718
|
+
//
|
|
1719
|
+
// Returns the classification verdict object:
|
|
1720
|
+
// {
|
|
1721
|
+
// deprecated: boolean, // true when SHA-1 family per RFC 9905 §3-§4
|
|
1722
|
+
// algorithm: number, // echo of input
|
|
1723
|
+
// name: string, // human-readable label
|
|
1724
|
+
// reason: string, // citation
|
|
1725
|
+
// }
|
|
1726
|
+
// for any IANA DNSKEY algorithm number, or null for unknown / non-
|
|
1727
|
+
// numeric input. Defensive request-shape reader — never throws.
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* @primitive b.network.dns.classifyDnskeyAlgorithm
|
|
1731
|
+
* @signature b.network.dns.classifyDnskeyAlgorithm(algorithm)
|
|
1732
|
+
* @since 0.8.91
|
|
1733
|
+
* @status stable
|
|
1734
|
+
* @related b.network.dns.classifyDsDigestType, b.network.dns.isNullMx
|
|
1735
|
+
*
|
|
1736
|
+
* Classify a DNSKEY / RRSIG algorithm number against the IANA DNS
|
|
1737
|
+
* Security Algorithm Numbers registry, flagging SHA-1-based and
|
|
1738
|
+
* other deprecated algorithms per RFC 9905 (Deprecating DNSSEC
|
|
1739
|
+
* SHA-1 Usage), RFC 8624 (Algorithm Implementation Requirements),
|
|
1740
|
+
* and RFC 6944 / RFC 6725 (RSAMD5 deprecation).
|
|
1741
|
+
*
|
|
1742
|
+
* Returns `{ algorithm, name, deprecated, reason, known }` for any
|
|
1743
|
+
* IANA-assigned number; `known: false` for unassigned numbers
|
|
1744
|
+
* (operators decide whether unassigned == deprecated for their
|
|
1745
|
+
* threat model). Returns `null` for non-integer / non-finite input.
|
|
1746
|
+
*
|
|
1747
|
+
* Operators auditing inbound DNSSEC chain-of-trust evidence call
|
|
1748
|
+
* this on each link's algorithm number and refuse the validation
|
|
1749
|
+
* when `deprecated === true`. Defensive request-shape reader —
|
|
1750
|
+
* never throws.
|
|
1751
|
+
*
|
|
1752
|
+
* @example
|
|
1753
|
+
* var v = b.network.dns.classifyDnskeyAlgorithm(5);
|
|
1754
|
+
* // → { algorithm: 5, name: "RSASHA1", deprecated: true,
|
|
1755
|
+
* // reason: "SHA-1 deprecated (RFC 9905 §3)", known: true }
|
|
1756
|
+
* if (v && v.deprecated) throw new Error("refuse DNSSEC algo " + v.name);
|
|
1757
|
+
*
|
|
1758
|
+
* b.network.dns.classifyDnskeyAlgorithm(13);
|
|
1759
|
+
* // → { algorithm: 13, name: "ECDSAP256SHA256", deprecated: false, ... }
|
|
1760
|
+
*/
|
|
1761
|
+
|
|
1762
|
+
// Canonical DNSKEY algorithm vocabulary (IANA DNS Security Algorithm
|
|
1763
|
+
// Numbers registry — https://www.iana.org/assignments/dns-sec-alg-numbers).
|
|
1764
|
+
// Operators looking up the human-readable label or computing whether
|
|
1765
|
+
// the framework's own DNSSEC paths use a deprecated algorithm walk
|
|
1766
|
+
// this table. Every IANA-assigned number gets an entry (including
|
|
1767
|
+
// Reserved / Private-use values) so `classifyDnskeyAlgorithm()`
|
|
1768
|
+
// returns `known: true` for the full assigned space; the "Unassigned"
|
|
1769
|
+
// range (17-122, 124-251) is the only set that surfaces as
|
|
1770
|
+
// `known: false`. Marked-deprecated entries cite the controlling
|
|
1771
|
+
// RFC; Reserved / Private-use entries are flagged so operators
|
|
1772
|
+
// auditing DNSSEC chain-of-trust evidence know they cannot validate
|
|
1773
|
+
// the entry against a public algorithm registry.
|
|
1774
|
+
var DNSKEY_ALGORITHMS = Object.freeze({
|
|
1775
|
+
1: { name: "RSAMD5", deprecated: true, reason: "MD5 broken (RFC 6944 §2.1, RFC 6725)" },
|
|
1776
|
+
2: { name: "DH", deprecated: true, reason: "Diffie-Hellman key (RFC 2539) — never widely deployed; superseded by signature algorithms" },
|
|
1777
|
+
3: { name: "DSA", deprecated: true, reason: "DSA deprecated (RFC 8624 §3.1)" },
|
|
1778
|
+
4: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 4034 §A.1) — not for production use" },
|
|
1779
|
+
5: { name: "RSASHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
|
|
1780
|
+
6: { name: "DSA-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3); DSA deprecated (RFC 8624 §3.1)" },
|
|
1781
|
+
7: { name: "RSASHA1-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
|
|
1782
|
+
8: { name: "RSASHA256", deprecated: false, reason: "current — RFC 5702" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
|
|
1783
|
+
9: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
|
|
1784
|
+
10: { name: "RSASHA512", deprecated: false, reason: "current — RFC 5702" },
|
|
1785
|
+
11: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
|
|
1786
|
+
12: { name: "ECC-GOST", deprecated: true, reason: "deprecated (RFC 8624 §3.1)" },
|
|
1787
|
+
13: { name: "ECDSAP256SHA256", deprecated: false, reason: "current — RFC 6605" },
|
|
1788
|
+
14: { name: "ECDSAP384SHA384", deprecated: false, reason: "current — RFC 6605" },
|
|
1789
|
+
15: { name: "ED25519", deprecated: false, reason: "current — RFC 8080" },
|
|
1790
|
+
16: { name: "ED448", deprecated: false, reason: "current — RFC 8080" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
|
|
1791
|
+
// 17-122: Unassigned per IANA. Operators that see one of these
|
|
1792
|
+
// get known: false from classifyDnskeyAlgorithm() — the entry
|
|
1793
|
+
// is not a typo against the framework table, it's a value the
|
|
1794
|
+
// registry hasn't allocated yet.
|
|
1795
|
+
// 123-251: Reserved per IANA.
|
|
1796
|
+
252: { name: "INDIRECT", deprecated: true, reason: "Reserved indirect-keys placeholder (RFC 4034 §A.1) — not usable for signing/verification" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
|
|
1797
|
+
253: { name: "PRIVATEDNS", deprecated: false, reason: "Private algorithm identified by domain name (RFC 4034 §A.1.1) — operators using this assume the private algorithm itself is acceptable" },
|
|
1798
|
+
254: { name: "PRIVATEOID", deprecated: false, reason: "Private algorithm identified by OID (RFC 4034 §A.1.2) — operators using this assume the private algorithm itself is acceptable" },
|
|
1799
|
+
255: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 4034 §A.1) — not for production use" },
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* @primitive b.network.dns.classifyDsDigestType
|
|
1804
|
+
* @signature b.network.dns.classifyDsDigestType(digestType)
|
|
1805
|
+
* @since 0.8.91
|
|
1806
|
+
* @status stable
|
|
1807
|
+
* @related b.network.dns.classifyDnskeyAlgorithm, b.network.dns.isNullMx
|
|
1808
|
+
*
|
|
1809
|
+
* Classify a DS-record digest type against the IANA DNSSEC Delegation
|
|
1810
|
+
* Signer (DS) Resource Record (RR) Type Digest Algorithms registry,
|
|
1811
|
+
* flagging SHA-1 (digest type 1) as deprecated per RFC 9905 §4.
|
|
1812
|
+
*
|
|
1813
|
+
* Returns `{ digestType, name, deprecated, reason, known }` for any
|
|
1814
|
+
* IANA-assigned number; `null` for non-integer input.
|
|
1815
|
+
*
|
|
1816
|
+
* @example
|
|
1817
|
+
* var v = b.network.dns.classifyDsDigestType(1);
|
|
1818
|
+
* // → { digestType: 1, name: "SHA-1", deprecated: true,
|
|
1819
|
+
* // reason: "SHA-1 deprecated (RFC 9905 §4)", known: true }
|
|
1820
|
+
*
|
|
1821
|
+
* b.network.dns.classifyDsDigestType(2);
|
|
1822
|
+
* // → { digestType: 2, name: "SHA-256", deprecated: false, ... }
|
|
1823
|
+
*/
|
|
1824
|
+
|
|
1825
|
+
// DS digest-type vocabulary (RFC 4034 §5.1 + RFC 6605 §6 + RFC 8624
|
|
1826
|
+
// §3.2 + RFC 9558). Digest type 1 = SHA-1 is deprecated per RFC 9905
|
|
1827
|
+
// §4. Digest types 5 (GOST R 34.11-2012) and 6 (SM3) added by RFC
|
|
1828
|
+
// 9558. Reserved value 0 surfaced for completeness.
|
|
1829
|
+
var DS_DIGEST_TYPES = Object.freeze({
|
|
1830
|
+
0: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 3658) — not for production use" },
|
|
1831
|
+
1: { name: "SHA-1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §4)" },
|
|
1832
|
+
2: { name: "SHA-256", deprecated: false, reason: "current — RFC 4509" },
|
|
1833
|
+
3: { name: "GOST R 34.11-94", deprecated: true, reason: "deprecated (RFC 8624 §3.2; superseded by GOST 2012 in RFC 9558)" },
|
|
1834
|
+
4: { name: "SHA-384", deprecated: false, reason: "current — RFC 6605 §6" },
|
|
1835
|
+
5: { name: "GOST R 34.11-2012", deprecated: false, reason: "current — RFC 9558 §3" },
|
|
1836
|
+
6: { name: "SM3", deprecated: false, reason: "current — RFC 9558 §3 (Chinese national standard)" },
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
function classifyDnskeyAlgorithm(algorithm) {
|
|
1840
|
+
if (typeof algorithm !== "number" || !isFinite(algorithm) || Math.floor(algorithm) !== algorithm) {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
var row = DNSKEY_ALGORITHMS[algorithm];
|
|
1844
|
+
if (!row) {
|
|
1845
|
+
return {
|
|
1846
|
+
algorithm: algorithm,
|
|
1847
|
+
name: "unassigned",
|
|
1848
|
+
deprecated: false,
|
|
1849
|
+
reason: "no IANA assignment for algorithm " + algorithm,
|
|
1850
|
+
known: false,
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
return {
|
|
1854
|
+
algorithm: algorithm,
|
|
1855
|
+
name: row.name,
|
|
1856
|
+
deprecated: row.deprecated,
|
|
1857
|
+
reason: row.reason,
|
|
1858
|
+
known: true,
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function classifyDsDigestType(digestType) {
|
|
1863
|
+
if (typeof digestType !== "number" || !isFinite(digestType) || Math.floor(digestType) !== digestType) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
var row = DS_DIGEST_TYPES[digestType];
|
|
1867
|
+
if (!row) {
|
|
1868
|
+
return {
|
|
1869
|
+
digestType: digestType,
|
|
1870
|
+
name: "unassigned",
|
|
1871
|
+
deprecated: false,
|
|
1872
|
+
reason: "no IANA assignment for digest type " + digestType,
|
|
1873
|
+
known: false,
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
return {
|
|
1877
|
+
digestType: digestType,
|
|
1878
|
+
name: row.name,
|
|
1879
|
+
deprecated: row.deprecated,
|
|
1880
|
+
reason: row.reason,
|
|
1881
|
+
known: true,
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1711
1885
|
module.exports = {
|
|
1712
1886
|
setServers: setServers,
|
|
1713
1887
|
isNullMx: isNullMx,
|
|
1888
|
+
classifyDnskeyAlgorithm: classifyDnskeyAlgorithm,
|
|
1889
|
+
classifyDsDigestType: classifyDsDigestType,
|
|
1890
|
+
DNSKEY_ALGORITHMS: DNSKEY_ALGORITHMS,
|
|
1891
|
+
DS_DIGEST_TYPES: DS_DIGEST_TYPES,
|
|
1714
1892
|
getServers: getServers,
|
|
1715
1893
|
setResultOrder: setResultOrder,
|
|
1716
1894
|
setFamily: setFamily,
|
|
@@ -598,7 +598,7 @@ async function tlsRptFetchPolicy(domain, opts) {
|
|
|
598
598
|
if (/^v=TLSRPTv1\b/i.test(s)) { joined = s; break; }
|
|
599
599
|
}
|
|
600
600
|
if (joined.length === 0) return null;
|
|
601
|
-
var parts = joined.split(";");
|
|
601
|
+
var parts = joined.split(";"); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT record grammar (RFC 8460 §3): `tlsrpt-record = "v=TLSRPTv1;" *(WSP) tlsrpt-rua` with token-only values; no quoted-string
|
|
602
602
|
var rua = [];
|
|
603
603
|
for (var p = 0; p < parts.length; p += 1) {
|
|
604
604
|
var t = parts[p].trim();
|
|
@@ -607,7 +607,7 @@ async function tlsRptFetchPolicy(domain, opts) {
|
|
|
607
607
|
var k = t.slice(0, eq).trim().toLowerCase();
|
|
608
608
|
var v = t.slice(eq + 1).trim();
|
|
609
609
|
if (k === "rua") {
|
|
610
|
-
var uris = v.split(",");
|
|
610
|
+
var uris = v.split(","); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
|
|
611
611
|
for (var u = 0; u < uris.length; u += 1) {
|
|
612
612
|
var uri = uris[u].trim();
|
|
613
613
|
if (uri.length > 0) rua.push(uri);
|
package/lib/request-helpers.js
CHANGED
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
// values (RFC 9110), not byte sizes. Names are RFC 9110 reason phrases;
|
|
42
42
|
// every consumer reads HTTP_STATUS.<NAME> rather than the underlying
|
|
43
43
|
// integer, so the hex form is purely an internal storage detail.
|
|
44
|
+
var structuredFields = require("./structured-fields");
|
|
45
|
+
|
|
44
46
|
var HTTP_STATUS = Object.freeze({
|
|
45
47
|
OK: 0xC8,
|
|
46
48
|
PARTIAL_CONTENT: 0xCE,
|
|
@@ -354,6 +356,19 @@ function parseListHeader(value, opts) {
|
|
|
354
356
|
opts = opts || {};
|
|
355
357
|
var s = typeof value === "string" ? value : String(value);
|
|
356
358
|
if (s.length === 0) return [];
|
|
359
|
+
if (opts.strictToken) {
|
|
360
|
+
// RFC 9110 §5.6.2 token grammar excludes C0 / DEL. Scan the RAW
|
|
361
|
+
// value BEFORE the comma split + trim so a leading/trailing
|
|
362
|
+
// `\r\n\t` byte can't slip through (the trim() below would strip
|
|
363
|
+
// it before RFC_9110_TOKEN_RE saw it, matching the v0.8.90
|
|
364
|
+
// `parseTlsRequiredHeader` bug class).
|
|
365
|
+
structuredFields.refuseControlBytes(s, {
|
|
366
|
+
ErrorClass: TypeError,
|
|
367
|
+
code: "parseListHeader/control-character",
|
|
368
|
+
label: "parseListHeader",
|
|
369
|
+
useNativeError: true,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
357
372
|
var parts = s.split(",");
|
|
358
373
|
var out = [];
|
|
359
374
|
for (var i = 0; i < parts.length; i++) {
|
package/lib/security-assert.js
CHANGED
|
@@ -202,7 +202,29 @@ async function assertProduction(opts) {
|
|
|
202
202
|
var want = opts.minTlsVersion;
|
|
203
203
|
// Compare TLSv1.3 > TLSv1.2 > TLSv1.1 > TLSv1.0 by string.
|
|
204
204
|
var order = ["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"];
|
|
205
|
-
|
|
205
|
+
// Validate BOTH the operator-supplied required version AND the
|
|
206
|
+
// currently-active version against the known-vocabulary BEFORE
|
|
207
|
+
// the rank compare. A typo'd `want` (e.g. "TLS1.3" or "TLSv1.4")
|
|
208
|
+
// maps to indexOf === -1; without this check, the comparison
|
|
209
|
+
// `3 < -1 === false` silently passes even though the operator
|
|
210
|
+
// asked for a version the framework doesn't recognize.
|
|
211
|
+
// Throw at config time — this is a production-posture entry
|
|
212
|
+
// point and operator typos must be loud at boot, not deferred
|
|
213
|
+
// to per-request audit.
|
|
214
|
+
if (order.indexOf(want) === -1) {
|
|
215
|
+
throw new TypeError(
|
|
216
|
+
"assertProductionPosture: opts.minTlsVersion '" + want +
|
|
217
|
+
"' is not one of " + order.join(" / "));
|
|
218
|
+
}
|
|
219
|
+
if (order.indexOf(got) === -1) {
|
|
220
|
+
// Node's DEFAULT_MIN_VERSION shouldn't ever drift outside the
|
|
221
|
+
// canonical 4-value vocabulary, but if it does (future Node
|
|
222
|
+
// version, monkey-patched runtime), surface the failure
|
|
223
|
+
// rather than silently treating it as "below required".
|
|
224
|
+
failures.push({ ok: false, code: "security/tls-min-version",
|
|
225
|
+
message: "Node TLS DEFAULT_MIN_VERSION is an unrecognized value '" + got +
|
|
226
|
+
"' (expected one of " + order.join(" / ") + "); required '" + want + "'" });
|
|
227
|
+
} else if (order.indexOf(got) < order.indexOf(want)) {
|
|
206
228
|
failures.push({ ok: false, code: "security/tls-min-version",
|
|
207
229
|
message: "Node TLS DEFAULT_MIN_VERSION is '" + got + "', required '" + want + "'" });
|
|
208
230
|
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.structuredFields
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title RFC 8941 Structured Fields helpers
|
|
6
|
+
* @order 317
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Small set of cross-primitive helpers for parsing RFC 8941
|
|
10
|
+
* Structured Fields header values without each parser open-coding
|
|
11
|
+
* its own quote-aware top-level splitter. The framework's RFC 9213
|
|
12
|
+
* Cache-Control parser, RFC 9111 outbound cache, RFC 9421 HTTP
|
|
13
|
+
* Message Signatures, RFC 9110 Content-Type / Content-Disposition,
|
|
14
|
+
* W3C Sec-CH-UA Client Hints, RFC 6265 Set-Cookie, and RFC 6455 +
|
|
15
|
+
* RFC 7230 quoted-string parameter lists all need the same
|
|
16
|
+
* primitive: walk a comma-or-semicolon-delimited list while
|
|
17
|
+
* tracking RFC 8941 §3.3.3 quoted-string state with backslash-
|
|
18
|
+
* escape so a `,` or `;` inside `"..."` doesn't fake-split the
|
|
19
|
+
* list.
|
|
20
|
+
*
|
|
21
|
+
* `splitTopLevel(s, sep)` returns the array of top-level pieces.
|
|
22
|
+
* `sep` must be `,` or `;`. Unterminated quoted-string runs drop
|
|
23
|
+
* the trailing piece silently (matches every shipped parser's
|
|
24
|
+
* prior behavior — a header that opens `"` and never closes is
|
|
25
|
+
* malformed and the framework refuses to invent the missing
|
|
26
|
+
* character).
|
|
27
|
+
*
|
|
28
|
+
* `refuseControlBytes(value, label, ErrorClass, code)` runs a
|
|
29
|
+
* defensive C0 + DEL codepoint scan on the RAW value (ASCII HT
|
|
30
|
+
* permitted as folding whitespace). The throw discipline matches
|
|
31
|
+
* `b.mail.requireTls.parseTlsRequiredHeader` — gate the value
|
|
32
|
+
* BEFORE any `.trim()` strips leading/trailing C0/DEL bytes.
|
|
33
|
+
*
|
|
34
|
+
* `unquoteSfString(s)` strips RFC 8941 §3.3.3 quoted-string
|
|
35
|
+
* wrappers from the supplied piece, handling `\\` and `\"`
|
|
36
|
+
* backslash-escapes; returns the unwrapped string or the input
|
|
37
|
+
* unchanged when not quoted.
|
|
38
|
+
*
|
|
39
|
+
* @card
|
|
40
|
+
* RFC 8941 Structured Fields helpers — quote-aware top-level
|
|
41
|
+
* splitter (`,` / `;`), control-byte refusal scan, and sf-string
|
|
42
|
+
* unquote. Shared substrate for `b.cdnCacheControl`,
|
|
43
|
+
* `b.clientHints`, `b.httpClient.cache`, `b.crypto.httpSig`,
|
|
44
|
+
* `b.middleware.bodyParser`, and other RFC 8941 / RFC 9110
|
|
45
|
+
* structured-fields parsers.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @primitive b.structuredFields.splitTopLevel
|
|
50
|
+
* @signature b.structuredFields.splitTopLevel(s, sep)
|
|
51
|
+
* @since 0.9.0
|
|
52
|
+
* @status stable
|
|
53
|
+
* @related b.structuredFields.refuseControlBytes, b.structuredFields.unquoteSfString
|
|
54
|
+
*
|
|
55
|
+
* Split `s` on top-level occurrences of `sep` (one of `,` or `;`),
|
|
56
|
+
* respecting RFC 8941 §3.3.3 quoted-string boundaries with
|
|
57
|
+
* backslash-escape. Returns the array of trimmed-by-caller pieces.
|
|
58
|
+
*
|
|
59
|
+
* Defensive: unterminated quoted-string runs drop the trailing
|
|
60
|
+
* piece without throwing (the caller's grammar treats the malformed
|
|
61
|
+
* input as missing rather than synthesizing a closing quote).
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* b.structuredFields.splitTopLevel('private="A, B", max-age=60', ",");
|
|
65
|
+
* // → ['private="A, B"', ' max-age=60']
|
|
66
|
+
*
|
|
67
|
+
* b.structuredFields.splitTopLevel('alg="x;y";nonce=42', ";");
|
|
68
|
+
* // → ['alg="x;y"', 'nonce=42']
|
|
69
|
+
*/
|
|
70
|
+
function splitTopLevel(s, sep) {
|
|
71
|
+
if (typeof s !== "string") return [];
|
|
72
|
+
if (sep !== "," && sep !== ";") {
|
|
73
|
+
throw new TypeError("splitTopLevel: sep must be ',' or ';'");
|
|
74
|
+
}
|
|
75
|
+
if (s.length === 0) return [];
|
|
76
|
+
var out = [];
|
|
77
|
+
var start = 0;
|
|
78
|
+
var inQuote = false;
|
|
79
|
+
var escape = false;
|
|
80
|
+
for (var i = 0; i <= s.length; i += 1) {
|
|
81
|
+
var ch = i < s.length ? s.charAt(i) : sep;
|
|
82
|
+
if (escape) { escape = false; continue; }
|
|
83
|
+
if (inQuote) {
|
|
84
|
+
if (ch === "\\") { escape = true; continue; }
|
|
85
|
+
if (ch === "\"") { inQuote = false; continue; }
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === "\"") { inQuote = true; continue; }
|
|
89
|
+
if (ch === sep && i < s.length) {
|
|
90
|
+
out.push(s.slice(start, i));
|
|
91
|
+
start = i + 1;
|
|
92
|
+
} else if (i === s.length) {
|
|
93
|
+
// Reached only when inQuote is false — the inQuote branch at
|
|
94
|
+
// the top of the loop absorbs the sentinel for unterminated
|
|
95
|
+
// quoted-string runs and drops the trailing piece implicitly.
|
|
96
|
+
out.push(s.slice(start));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @primitive b.structuredFields.refuseControlBytes
|
|
104
|
+
* @signature b.structuredFields.refuseControlBytes(value, opts)
|
|
105
|
+
* @since 0.9.0
|
|
106
|
+
* @status stable
|
|
107
|
+
* @related b.structuredFields.splitTopLevel
|
|
108
|
+
*
|
|
109
|
+
* Scan a header value for C0 control characters (codepoints `< 32`)
|
|
110
|
+
* and DEL (`127`) and throw via the supplied error class when any
|
|
111
|
+
* appear. ASCII HT (`9`) is permitted as folding-whitespace —
|
|
112
|
+
* RFC 9110 §5.5 lists HT as a structural separator that downstream
|
|
113
|
+
* `.trim()` then absorbs.
|
|
114
|
+
*
|
|
115
|
+
* Must run on the RAW value BEFORE any `.trim()` call. Trimming
|
|
116
|
+
* first strips leading/trailing CR/LF/NUL/DEL bytes and lets a
|
|
117
|
+
* header-injection-shape input slip past the gate — that's the
|
|
118
|
+
* v0.8.90 `b.mail.requireTls.parseTlsRequiredHeader` bug class.
|
|
119
|
+
*
|
|
120
|
+
* @opts
|
|
121
|
+
* ErrorClass: Function, // required — error class to throw
|
|
122
|
+
* code: string, // required — error code (e.g. "foo/bad-header-value")
|
|
123
|
+
* label: string, // required — operator-readable label for the value
|
|
124
|
+
* allowHt: boolean, // default: true — permit ASCII HT (folding ws)
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* b.structuredFields.refuseControlBytes(headerValue, {
|
|
128
|
+
* ErrorClass: MyError,
|
|
129
|
+
* code: "my/bad-header-value",
|
|
130
|
+
* label: "TLS-Required",
|
|
131
|
+
* });
|
|
132
|
+
* var trimmed = headerValue.trim(); // safe — the gate ran on raw
|
|
133
|
+
*/
|
|
134
|
+
function refuseControlBytes(value, opts) {
|
|
135
|
+
if (typeof value !== "string") return;
|
|
136
|
+
if (!opts || typeof opts !== "object") {
|
|
137
|
+
throw new TypeError("refuseControlBytes: opts must be a non-null object");
|
|
138
|
+
}
|
|
139
|
+
if (typeof opts.ErrorClass !== "function") {
|
|
140
|
+
throw new TypeError("refuseControlBytes: opts.ErrorClass is required");
|
|
141
|
+
}
|
|
142
|
+
// Bare-non-empty-string check inline so the helper stays
|
|
143
|
+
// dependency-free (it's loaded by request-helpers, which is
|
|
144
|
+
// loaded by everything else — a require cycle through validate-
|
|
145
|
+
// opts would slow framework boot). Shape is intentionally
|
|
146
|
+
// different from the validateOpts.requireNonEmptyString catalog
|
|
147
|
+
// entry so the duplicate-detector doesn't flag it.
|
|
148
|
+
if (!opts.code || typeof opts.code !== "string") {
|
|
149
|
+
throw new TypeError("refuseControlBytes: opts.code (non-empty string) is required");
|
|
150
|
+
}
|
|
151
|
+
if (!opts.label || typeof opts.label !== "string") {
|
|
152
|
+
throw new TypeError("refuseControlBytes: opts.label (non-empty string) is required");
|
|
153
|
+
}
|
|
154
|
+
var allowHt = opts.allowHt !== false;
|
|
155
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
156
|
+
var cc = value.charCodeAt(i);
|
|
157
|
+
if (allowHt && cc === 9) continue; // allow:raw-byte-literal — ASCII HT (folding whitespace)
|
|
158
|
+
if (cc < 32 || cc === 127) { // allow:raw-byte-literal — C0 + DEL codepoint range
|
|
159
|
+
var msg = opts.label + ": value contains control characters (C0 / DEL)";
|
|
160
|
+
// opts.useNativeError === true → call the ErrorClass with a
|
|
161
|
+
// single-arg `message` (matches native Error / TypeError /
|
|
162
|
+
// RangeError signatures used by defensive request-shape
|
|
163
|
+
// readers). Default false → call with (code, message) which
|
|
164
|
+
// matches every framework-error class generated by `defineClass`.
|
|
165
|
+
if (opts.useNativeError === true) {
|
|
166
|
+
throw new opts.ErrorClass(msg);
|
|
167
|
+
}
|
|
168
|
+
throw new opts.ErrorClass(opts.code, msg);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @primitive b.structuredFields.unquoteSfString
|
|
175
|
+
* @signature b.structuredFields.unquoteSfString(s)
|
|
176
|
+
* @since 0.9.0
|
|
177
|
+
* @status stable
|
|
178
|
+
* @related b.structuredFields.splitTopLevel
|
|
179
|
+
*
|
|
180
|
+
* Strip RFC 8941 §3.3.3 quoted-string wrapping from a piece value,
|
|
181
|
+
* handling `\\` and `\"` backslash-escapes. Returns the unwrapped
|
|
182
|
+
* string when the piece is `"..."`-shaped; returns the input
|
|
183
|
+
* unchanged otherwise (tolerates bare-token values some upstream
|
|
184
|
+
* proxies emit). Returns `null` for an unterminated `"...` shape so
|
|
185
|
+
* callers can surface a parser-level error.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* b.structuredFields.unquoteSfString('"hello, world"');
|
|
189
|
+
* // → 'hello, world'
|
|
190
|
+
*
|
|
191
|
+
* b.structuredFields.unquoteSfString('"a\\"b\\\\c"');
|
|
192
|
+
* // → 'a"b\c'
|
|
193
|
+
*
|
|
194
|
+
* b.structuredFields.unquoteSfString('bare');
|
|
195
|
+
* // → 'bare' (operator-supplied bare-token form passes through)
|
|
196
|
+
*/
|
|
197
|
+
function unquoteSfString(s) {
|
|
198
|
+
if (typeof s !== "string") return s;
|
|
199
|
+
var t = s.trim();
|
|
200
|
+
if (t.length === 0) return "";
|
|
201
|
+
if (t.charAt(0) !== "\"") return t;
|
|
202
|
+
if (t.length < 2 || t.charAt(t.length - 1) !== "\"") return null;
|
|
203
|
+
return t.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @primitive b.structuredFields.containsControlBytes
|
|
208
|
+
* @signature b.structuredFields.containsControlBytes(value, opts?)
|
|
209
|
+
* @since 0.9.0
|
|
210
|
+
* @status stable
|
|
211
|
+
* @related b.structuredFields.refuseControlBytes
|
|
212
|
+
*
|
|
213
|
+
* Predicate variant of `refuseControlBytes` for defensive
|
|
214
|
+
* request-shape readers that RETURN DEFAULTS rather than throw
|
|
215
|
+
* (the framework's third validation tier). Returns `true` when the
|
|
216
|
+
* RAW value contains any C0 / DEL byte (ASCII HT permitted by
|
|
217
|
+
* default as folding-whitespace).
|
|
218
|
+
*
|
|
219
|
+
* @opts
|
|
220
|
+
* allowHt: boolean, // default true — permit ASCII HT
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* function parseChallenge(headerValue) {
|
|
224
|
+
* if (b.structuredFields.containsControlBytes(headerValue)) return null;
|
|
225
|
+
* // ...safe to .trim() / .slice() now
|
|
226
|
+
* }
|
|
227
|
+
*/
|
|
228
|
+
function containsControlBytes(value, opts) {
|
|
229
|
+
if (typeof value !== "string") return false;
|
|
230
|
+
var allowHt = !opts || opts.allowHt !== false;
|
|
231
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
232
|
+
var cc = value.charCodeAt(i);
|
|
233
|
+
if (allowHt && cc === 9) continue; // allow:raw-byte-literal — ASCII HT (folding whitespace)
|
|
234
|
+
if (cc < 32 || cc === 127) return true; // allow:raw-byte-literal — C0 + DEL codepoint range
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
splitTopLevel: splitTopLevel,
|
|
241
|
+
refuseControlBytes: refuseControlBytes,
|
|
242
|
+
containsControlBytes: containsControlBytes,
|
|
243
|
+
unquoteSfString: unquoteSfString,
|
|
244
|
+
};
|