@blamejs/core 0.18.38 → 0.18.40
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 +121 -1
- package/NOTICE +2 -2
- package/README.md +1 -1
- package/lib/app.js +14 -5
- package/lib/file-upload.js +17 -3
- package/lib/gate-contract.js +159 -5
- package/lib/guard-filename.js +93 -18
- package/lib/guard-sql.js +7 -1
- package/lib/guard-yaml.js +64 -0
- package/lib/mail-auth.js +473 -73
- package/lib/network-dns-resolver.js +23 -23
- package/lib/network-dns.js +205 -43
- package/lib/public-suffix.js +110 -24
- package/lib/vendor/MANIFEST.json +15 -15
- package/lib/vendor/blamejs-pki.cjs +2120 -508
- package/lib/vendor/public-suffix-list.dat +4 -3
- package/lib/vendor/public-suffix-list.data.js +2201 -2201
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -278,6 +278,22 @@ function create(opts) {
|
|
|
278
278
|
throw new ResolverError("resolver/bad-input",
|
|
279
279
|
"query: unknown qtype '" + type + "'");
|
|
280
280
|
}
|
|
281
|
+
// Canonicalize BEFORE keying. A U-label and its A-label are one DNS name,
|
|
282
|
+
// so keying on the raw string splits it across two cache entries and sends
|
|
283
|
+
// a U-label upstream that no zone answers for; the same pass refuses a name
|
|
284
|
+
// carrying an empty label rather than letting the wire encoder decide. The
|
|
285
|
+
// check lives in b.network.dns so this layer and the transport cannot
|
|
286
|
+
// disagree about what counts as a name.
|
|
287
|
+
// Translated, not propagated: b.network.dns raises DnsError/`dns/bad-host`,
|
|
288
|
+
// and every other input refusal on this API is a ResolverError with a
|
|
289
|
+
// `resolver/*` code. Letting the transport's error class out through the
|
|
290
|
+
// resolver's surface would leave a caller that classifies by code or class
|
|
291
|
+
// unable to recognize a bad name as bad input.
|
|
292
|
+
try { name = networkDns._validateHostShape(name, "resolver.query"); }
|
|
293
|
+
catch (e) {
|
|
294
|
+
throw new ResolverError("resolver/bad-input",
|
|
295
|
+
"query: " + ((e && e.message) || "name is not a valid DNS name"));
|
|
296
|
+
}
|
|
281
297
|
var validate = qopts.validate === true;
|
|
282
298
|
var key = _key(name, qtype);
|
|
283
299
|
|
|
@@ -588,30 +604,14 @@ async function _wireLookup(name, qtype, timeoutMs) {
|
|
|
588
604
|
}
|
|
589
605
|
|
|
590
606
|
// _encodeWireQuery — assemble a wire-format DNS query for (name, qtype).
|
|
591
|
-
//
|
|
592
|
-
//
|
|
607
|
+
//
|
|
608
|
+
// The encoder itself lives in network-dns.js. This was a byte-for-byte copy of
|
|
609
|
+
// it, which meant the label validation added there — the 1..63 cap that stops a
|
|
610
|
+
// hostname putting a forged compression pointer in the question section, and
|
|
611
|
+
// the refusal to drop an empty label and ask for a neighbouring domain — would
|
|
612
|
+
// have had to be added twice to hold.
|
|
593
613
|
function _encodeWireQuery(name, qtype) {
|
|
594
|
-
|
|
595
|
-
var nameLen = 1;
|
|
596
|
-
for (var i = 0; i < parts.length; i += 1) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
|
|
597
|
-
var buf = Buffer.alloc(12 + nameLen + 4); // RFC 1035 §4.1.1 header (12) + question tail (4) + name
|
|
598
|
-
var id = bCrypto.randomInt(0, 0x10000); // RFC 1035 §4.1.1 16-bit query ID space
|
|
599
|
-
buf.writeUInt16BE(id, 0);
|
|
600
|
-
buf.writeUInt16BE(0x0100, 2); // RFC 1035 §4.1.1 RD=1 flags
|
|
601
|
-
buf.writeUInt16BE(1, 4); // RFC 1035 §4.1.1 qdcount
|
|
602
|
-
var off = 12; // RFC 1035 §4.1.1 header end / question start
|
|
603
|
-
for (var p = 0; p < parts.length; p += 1) {
|
|
604
|
-
var s = parts[p];
|
|
605
|
-
buf.writeUInt8(Buffer.byteLength(s, "ascii"), off);
|
|
606
|
-
off += 1;
|
|
607
|
-
off += buf.write(s, off, "ascii");
|
|
608
|
-
}
|
|
609
|
-
buf.writeUInt8(0, off);
|
|
610
|
-
off += 1;
|
|
611
|
-
buf.writeUInt16BE(qtype, off);
|
|
612
|
-
off += 2; // RFC 1035 §4.1.2 QTYPE width
|
|
613
|
-
buf.writeUInt16BE(1, off); // RFC 1035 §4.1.2 QCLASS=IN
|
|
614
|
-
return buf;
|
|
614
|
+
return networkDns._encodeDnsQuery(name, qtype).buf;
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
function _minTtl(rrs) {
|
package/lib/network-dns.js
CHANGED
|
@@ -13,6 +13,7 @@ var C = require("./constants");
|
|
|
13
13
|
var bCrypto = require("./crypto");
|
|
14
14
|
var lazyRequire = require("./lazy-require");
|
|
15
15
|
var safeBuffer = require("./safe-buffer");
|
|
16
|
+
var publicSuffix = require("./public-suffix");
|
|
16
17
|
var safeUrl = require("./safe-url");
|
|
17
18
|
var validateOpts = require("./validate-opts");
|
|
18
19
|
var { defineClass } = require("./framework-error");
|
|
@@ -129,25 +130,82 @@ function _labelsOf(host) {
|
|
|
129
130
|
// is one longer and encodes to identical wire bytes, so measuring the raw
|
|
130
131
|
// string would refuse a maximum-length name written absolutely while accepting
|
|
131
132
|
// it written relatively.
|
|
133
|
+
// Which character marks the root is not fixed: UTS #46 maps U+3002, U+FF0E and
|
|
134
|
+
// U+FF61 to ".", so `example。` is as absolute as `example.` and canonicalization
|
|
135
|
+
// turns one into the other. Every place that asks "does this name end in the
|
|
136
|
+
// root?" has to agree, or a spelling is accepted by one rule and refused by the
|
|
137
|
+
// next — the length cap measured a mapped marker as an extra character and
|
|
138
|
+
// refused a maximum-length name that its ASCII spelling passes.
|
|
139
|
+
function _endsWithRootMarker(host) {
|
|
140
|
+
if (typeof host !== "string" || host.length < 2) return false;
|
|
141
|
+
// Ask the primitive that owns the set. A second copy of these four characters
|
|
142
|
+
// drifts from it, and this module already learned that lesson once — the
|
|
143
|
+
// whole point of routing name questions through b.publicSuffix.
|
|
144
|
+
return publicSuffix._isRootMarker(host.charAt(host.length - 1));
|
|
145
|
+
}
|
|
146
|
+
|
|
132
147
|
function _hostLengthWithoutRoot(host) {
|
|
133
148
|
if (typeof host !== "string") return 0;
|
|
134
|
-
return (host
|
|
135
|
-
? host.length - 1
|
|
136
|
-
: host.length;
|
|
149
|
+
return _endsWithRootMarker(host) ? host.length - 1 : host.length;
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
function _validateHostShape(host, primitive) {
|
|
140
153
|
if (typeof host !== "string" || host.length === 0) return host;
|
|
141
154
|
if (net.isIP(host)) return host;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
// The root zone. `. NS` is how the root servers are asked for, and the name
|
|
156
|
+
// encodes as the empty label list, which the wire encoder already handles.
|
|
157
|
+
// It is the one name that is nothing BUT a root marker, so it must not reach
|
|
158
|
+
// `canonicalDomain` — that refuses a bare root, correctly, since for every
|
|
159
|
+
// other name a lone marker means the name went missing.
|
|
160
|
+
//
|
|
161
|
+
// Any of the four spellings, asked of the primitive that owns the set rather
|
|
162
|
+
// than compared against a copy of it: this module already treats U+3002 /
|
|
163
|
+
// U+FF0E / U+FF61 as equivalent to "." at the END of a name, so a root zone
|
|
164
|
+
// that depended on which one the caller typed would be the same name
|
|
165
|
+
// resolving through one spelling and not another. All four normalize to the
|
|
166
|
+
// ASCII form, as every other name here does.
|
|
167
|
+
if (host.length === 1 && publicSuffix._isRootMarker(host)) return ".";
|
|
168
|
+
// The same rules the wire encoder enforces, applied at the entry point so the
|
|
169
|
+
// caller is told which primitive refused the name. Checking only here would
|
|
170
|
+
// leave the encoder reachable from paths that do not pass through a public
|
|
171
|
+
// entry; checking only there would name no primitive.
|
|
172
|
+
_dnsQueryLabels(host, primitive);
|
|
173
|
+
// No short-circuit for ASCII. Returning such a name unchanged kept whatever
|
|
174
|
+
// case it was typed in, so `Example.COM` and `example.com` took separate
|
|
175
|
+
// resolver-cache entries and made separate upstream queries while putting
|
|
176
|
+
// byte-identical questions on the wire — the encoder lowercases either way.
|
|
177
|
+
// One canonical form for every alphabet is what makes the cache key the NAME
|
|
178
|
+
// rather than a spelling of it.
|
|
179
|
+
//
|
|
180
|
+
// An internationalized name is returned in its A-label form so that every
|
|
181
|
+
// reader downstream sees the same name the wire will carry. Converting only
|
|
182
|
+
// at the encoder would let `resolve4` reach the domain while `resolveSecure`
|
|
183
|
+
// and `querySvcb` refused it at their LDH pass, which has no reading of a
|
|
184
|
+
// U-label — the same domain resolving through one entry point and not
|
|
185
|
+
// another.
|
|
186
|
+
//
|
|
187
|
+
// The root marker is carried across rather than dropped: a resolver reads it
|
|
188
|
+
// as "already fully qualified, do not apply the search list", and under an
|
|
189
|
+
// elevated ndots losing it can resolve — and cache — a different name
|
|
190
|
+
// entirely.
|
|
191
|
+
//
|
|
192
|
+
// The marker is NOT removed here. canonicalDomain strips exactly one and
|
|
193
|
+
// refuses a doubled one, and a strip on this side is invisible to it — it
|
|
194
|
+
// would take the first marker, canonicalDomain would take the second, and a
|
|
195
|
+
// name with an empty final label would be quietly rewritten into a real,
|
|
196
|
+
// separately-owned one and then cached under it. `example.com。。` resolved as
|
|
197
|
+
// `example.com` that way, while the plain `example.com..` spelling of the
|
|
198
|
+
// same mistake was still refused.
|
|
199
|
+
//
|
|
200
|
+
// Absoluteness is settled by ASKING canonicalDomain rather than by reading
|
|
201
|
+
// the last character: UTS #46 DELETES 294 code points outright (U+00AD,
|
|
202
|
+
// U+200B, U+FEFF, U+2060, the variation selectors), so `example.com.` with
|
|
203
|
+
// one of them appended still ends in a root the raw character does not show.
|
|
204
|
+
// Appending one more marker makes a doubled one, which canonicalDomain
|
|
205
|
+
// refuses — so a refusal here means the name already carried its root.
|
|
206
|
+
var absolute = publicSuffix.canonicalDomain(host + ".") === "";
|
|
207
|
+
var ascii = _dnsToALabel(host, primitive);
|
|
208
|
+
return absolute ? ascii + "." : ascii;
|
|
151
209
|
}
|
|
152
210
|
|
|
153
211
|
// RFC 6761 special-form classification works on the label LIST, so the
|
|
@@ -427,8 +485,99 @@ function _armRequestTimeout(req, ms, host, reject) {
|
|
|
427
485
|
});
|
|
428
486
|
}
|
|
429
487
|
|
|
488
|
+
// RFC 1035 §2.3.4 — a label is 1..63 octets and an encoded name is at most 255.
|
|
489
|
+
var DNS_MAX_LABEL_OCTETS = 63;
|
|
490
|
+
var DNS_MAX_NAME_OCTETS = 255;
|
|
491
|
+
|
|
492
|
+
// Split a host into the labels the wire encoder will write, refusing anything
|
|
493
|
+
// the wire format cannot express. Every rejection here is a name that WOULD
|
|
494
|
+
// have been encoded as some other question:
|
|
495
|
+
//
|
|
496
|
+
// - An empty label. Dropping it turns `evil..example.com` into the real,
|
|
497
|
+
// separately-owned `evil.example.com`, so the query asks for a name the
|
|
498
|
+
// caller never passed and the answer is cached under the one they did.
|
|
499
|
+
// - A label over 63 octets. Its length goes into a single octet whose top two
|
|
500
|
+
// bits RFC 1035 §4.1.4 reserves: `11` marks a compression POINTER and `01`
|
|
501
|
+
// an unassigned label type (RFC 6891 §3). Writing the real length lets a
|
|
502
|
+
// 192-octet label put a forged pointer in the question section, aiming the
|
|
503
|
+
// upstream resolver's name parser at an offset the hostname chose.
|
|
504
|
+
// - A name over 255 octets encoded.
|
|
505
|
+
// One trailing root dot is legal and is the only thing removed; the bare root
|
|
506
|
+
// encodes as the empty label list it already was.
|
|
507
|
+
//
|
|
508
|
+
// An internationalized name is CONVERTED rather than refused. The wire carries
|
|
509
|
+
// A-labels, and `Buffer.write(s, "ascii")` keeps only a character's low byte,
|
|
510
|
+
// so writing a U-label directly would query a different name — but refusing one
|
|
511
|
+
// would fail a name that resolves perfectly well in its `xn--` form, which is
|
|
512
|
+
// how its owner published it.
|
|
513
|
+
// `publicSuffix.canonicalDomain` owns this conversion: raw `domainToASCII`
|
|
514
|
+
// TRUNCATES at a URL delimiter ("a.com/evil" -> "a.com"), which would turn a
|
|
515
|
+
// string that is not a bare host into a name the caller never asked for, and
|
|
516
|
+
// canonicalDomain refuses those instead. It also refuses a name carrying an
|
|
517
|
+
// empty label, which is the same answer this function gives for an ASCII one.
|
|
518
|
+
function _dnsToALabel(host, primitive) {
|
|
519
|
+
var ascii = publicSuffix.canonicalDomain(host);
|
|
520
|
+
if (!ascii) {
|
|
521
|
+
throw new DnsError("dns/bad-host",
|
|
522
|
+
primitive + ": internationalized host has no A-label (xn--) form");
|
|
523
|
+
}
|
|
524
|
+
return ascii;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function _dnsQueryLabels(host, primitive) {
|
|
528
|
+
var h = String(host);
|
|
529
|
+
if (h.length === 0 || h === ".") return [];
|
|
530
|
+
// EVERY name goes through the domain primitive, ASCII or not. This side used
|
|
531
|
+
// to keep its own, shorter idea of a valid name — empty label, non-ASCII,
|
|
532
|
+
// label 1..63, 255 total — and each character it did not think of was one it
|
|
533
|
+
// encoded into a query label while b.publicSuffix refused the same string
|
|
534
|
+
// outright: a NUL byte, every URL delimiter, and `%`, `^`, `|`, `<` besides.
|
|
535
|
+
// A delimiter is the one that bites, because `domainToASCII` TRUNCATES at
|
|
536
|
+
// one, so `example.com/evil` can masquerade as a trusted prefix of itself.
|
|
537
|
+
//
|
|
538
|
+
// Mirroring the rule was tried first, and the list of near-misses above is
|
|
539
|
+
// what that produced. Asking the owner is the version that cannot drift.
|
|
540
|
+
var canonical = publicSuffix.canonicalDomain(h);
|
|
541
|
+
if (!canonical) {
|
|
542
|
+
throw new DnsError("dns/bad-host",
|
|
543
|
+
primitive + ": host is not a valid domain name (empty label, control " +
|
|
544
|
+
"byte, URL delimiter, or over the RFC 1035 length ceiling)");
|
|
545
|
+
}
|
|
546
|
+
h = canonical;
|
|
547
|
+
// The label caps below still belong here: canonicalDomain bounds the whole
|
|
548
|
+
// NAME at 253 octets but says nothing about a single label, and 1..63 is what
|
|
549
|
+
// the wire format can express — a longer one writes a length octet the
|
|
550
|
+
// receiving parser reads as a compression pointer.
|
|
551
|
+
var labels = h.split(".");
|
|
552
|
+
var total = 1;
|
|
553
|
+
for (var i = 0; i < labels.length; i += 1) {
|
|
554
|
+
var len = Buffer.byteLength(labels[i], "utf8");
|
|
555
|
+
if (labels[i].length === 0) {
|
|
556
|
+
throw new DnsError("dns/bad-host",
|
|
557
|
+
primitive + ": host has an empty label — a name may carry one trailing " +
|
|
558
|
+
"root dot and no other empty label");
|
|
559
|
+
}
|
|
560
|
+
if (len !== labels[i].length) {
|
|
561
|
+
throw new DnsError("dns/bad-host",
|
|
562
|
+
primitive + ": host label is not ASCII and has no A-label (xn--) form");
|
|
563
|
+
}
|
|
564
|
+
if (len > DNS_MAX_LABEL_OCTETS) {
|
|
565
|
+
throw new DnsError("dns/bad-host",
|
|
566
|
+
primitive + ": host label is " + len + " octets (RFC 1035 allows 1.." +
|
|
567
|
+
DNS_MAX_LABEL_OCTETS + ")");
|
|
568
|
+
}
|
|
569
|
+
total += 1 + len;
|
|
570
|
+
}
|
|
571
|
+
if (total > DNS_MAX_NAME_OCTETS) {
|
|
572
|
+
throw new DnsError("dns/bad-host",
|
|
573
|
+
primitive + ": host encodes to " + total + " octets (RFC 1035 allows at most " +
|
|
574
|
+
DNS_MAX_NAME_OCTETS + ")");
|
|
575
|
+
}
|
|
576
|
+
return labels;
|
|
577
|
+
}
|
|
578
|
+
|
|
430
579
|
function _encodeDnsQuery(host, qtype) {
|
|
431
|
-
var parts = host
|
|
580
|
+
var parts = _dnsQueryLabels(host, "dns");
|
|
432
581
|
var nameLen = 1;
|
|
433
582
|
for (var i = 0; i < parts.length; i++) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
|
|
434
583
|
var buf = Buffer.alloc(12 + nameLen + 4);
|
|
@@ -676,29 +825,9 @@ async function resolveSecure(host, type) {
|
|
|
676
825
|
"resolveSecure requires DoH transport (call useDnsOverHttps " +
|
|
677
826
|
"or rely on the default-on DoH posture)");
|
|
678
827
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
"resolveSecure host is malformed");
|
|
683
|
-
}
|
|
684
|
-
// RFC 1035 §2.3.4 LDH validation — labels are letters / digits /
|
|
685
|
-
// hyphen, hyphens not at edges, label length 1..63, total length
|
|
686
|
-
// 253. Pre-v0.8.32 the framework only checked total length;
|
|
687
|
-
// operator-supplied hosts containing `_` / `:` / spaces flowed
|
|
688
|
-
// through to the DoH endpoint and surfaced as opaque server
|
|
689
|
-
// errors.
|
|
690
|
-
var labels = _labelsOf(host);
|
|
691
|
-
for (var li = 0; li < labels.length; li += 1) {
|
|
692
|
-
var label = labels[li];
|
|
693
|
-
if (label.length === 0 || label.length > 63) { // RFC 1035 max label length
|
|
694
|
-
throw new DnsError("dns/bad-host",
|
|
695
|
-
"resolveSecure host has invalid label (length 1..63 required, got " + label.length + ")");
|
|
696
|
-
}
|
|
697
|
-
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label)) {
|
|
698
|
-
throw new DnsError("dns/bad-host",
|
|
699
|
-
"resolveSecure host label '" + label + "' violates RFC 1035 LDH rule (letters/digits/hyphen, no leading/trailing hyphen)");
|
|
700
|
-
}
|
|
701
|
-
}
|
|
828
|
+
// An operator-supplied host containing `_` / `:` / a space would otherwise
|
|
829
|
+
// flow through to the DoH endpoint and surface as an opaque server error.
|
|
830
|
+
_validateLdh(host, "resolveSecure", false);
|
|
702
831
|
var family;
|
|
703
832
|
if (type === "A") family = 4;
|
|
704
833
|
else if (type === "AAAA") family = 6;
|
|
@@ -1316,14 +1445,27 @@ function _parseSvcbRdata(msg, rdataOff, rdlen) {
|
|
|
1316
1445
|
return { priority: priority, target: target, params: params };
|
|
1317
1446
|
}
|
|
1318
1447
|
|
|
1319
|
-
|
|
1448
|
+
// RFC 1035 §2.3.4 LDH validation — labels are letters / digits / hyphen, with
|
|
1449
|
+
// no hyphen at either edge. `allowUnderscore` additionally admits the leading
|
|
1450
|
+
// underscore that SVCB / HTTPS query targets carry ("_dns.resolver.arpa");
|
|
1451
|
+
// resolveSecure resolves ordinary hostnames and does not want it.
|
|
1452
|
+
//
|
|
1453
|
+
// This lives in one place because it did not: resolveSecure carried its own
|
|
1454
|
+
// copy of the loop, so the two drifted on which characters they accepted and a
|
|
1455
|
+
// rule added to either held for only half the primitives that need it.
|
|
1456
|
+
function _validateLdh(host, primitive, allowUnderscore) {
|
|
1320
1457
|
if (typeof host !== "string" || host.length === 0 ||
|
|
1321
1458
|
_hostLengthWithoutRoot(host) > 253) { // RFC 1035 hostname octet ceiling
|
|
1322
1459
|
throw new DnsError("dns/bad-host",
|
|
1323
1460
|
primitive + ": host must be a non-empty RFC 1035 LDH name (length 1..253)");
|
|
1324
1461
|
}
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1462
|
+
// The root zone has no labels at all — `_labelsOf(".")` yields one empty
|
|
1463
|
+
// string, which the 1..63 rule below refuses. Without this, a name the shape
|
|
1464
|
+
// check accepts is turned away here, so `. NS` works through the wire
|
|
1465
|
+
// resolver and not through `querySvcb` / `queryHttps` / `resolveSecure`: the
|
|
1466
|
+
// same name resolving through one entry point and not another, which is the
|
|
1467
|
+
// defect this module already fixed once for internationalized names.
|
|
1468
|
+
if (host === ".") return;
|
|
1327
1469
|
var labels = _labelsOf(host);
|
|
1328
1470
|
for (var li = 0; li < labels.length; li += 1) {
|
|
1329
1471
|
var label = labels[li];
|
|
@@ -1331,9 +1473,13 @@ function _validateLdh(host, primitive) {
|
|
|
1331
1473
|
throw new DnsError("dns/bad-host",
|
|
1332
1474
|
primitive + ": host label length must be 1..63");
|
|
1333
1475
|
}
|
|
1334
|
-
|
|
1476
|
+
var shaped = allowUnderscore
|
|
1477
|
+
? /^[A-Za-z0-9_](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?$/.test(label)
|
|
1478
|
+
: /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label);
|
|
1479
|
+
if (!shaped) {
|
|
1335
1480
|
throw new DnsError("dns/bad-host",
|
|
1336
|
-
primitive + ": host label '" + label + "' violates LDH (allowed: letters/digits
|
|
1481
|
+
primitive + ": host label '" + label + "' violates LDH (allowed: letters/digits" +
|
|
1482
|
+
(allowUnderscore ? "/underscore" : "") + "/hyphen, no leading/trailing hyphen)");
|
|
1337
1483
|
}
|
|
1338
1484
|
}
|
|
1339
1485
|
}
|
|
@@ -1342,7 +1488,7 @@ async function _querySvcbLike(host, qtype, opts) {
|
|
|
1342
1488
|
host = _validateHostShape(host, "dns.querySvcb");
|
|
1343
1489
|
opts = opts || {};
|
|
1344
1490
|
validateOpts(opts, ["transport"], "dns.querySvcb");
|
|
1345
|
-
_validateLdh(host, "dns.querySvcb");
|
|
1491
|
+
_validateLdh(host, "dns.querySvcb", true); // SVCB targets carry a leading underscore
|
|
1346
1492
|
if (opts.transport !== undefined && opts.transport !== "doh" &&
|
|
1347
1493
|
opts.transport !== "dot" && opts.transport !== "system") {
|
|
1348
1494
|
throw new DnsError("dns/bad-transport",
|
|
@@ -1488,7 +1634,16 @@ async function discoverEncrypted(opts) {
|
|
|
1488
1634
|
}
|
|
1489
1635
|
var insecureOnly = opts.insecureSystemResolverOnly !== false;
|
|
1490
1636
|
var transport = insecureOnly ? "system" : undefined;
|
|
1491
|
-
|
|
1637
|
+
// Canonicalize BEFORE the LDH pass. An LDH rule has no reading of a U-label,
|
|
1638
|
+
// so running it on the raw name refuses an internationalized one here while
|
|
1639
|
+
// the SVCB query underneath accepts it — the same name resolving through one
|
|
1640
|
+
// entry point and not its own caller.
|
|
1641
|
+
// Canonicalize BEFORE the LDH pass. An LDH rule has no reading of a U-label,
|
|
1642
|
+
// so running it on the raw name refuses an internationalized one here while
|
|
1643
|
+
// the SVCB query underneath accepts it — the same name resolving through one
|
|
1644
|
+
// entry point and not its own caller.
|
|
1645
|
+
name = _validateHostShape(name, "dns.discoverEncrypted");
|
|
1646
|
+
_validateLdh(name, "dns.discoverEncrypted", true); // "_dns.resolver.arpa" per RFC 9462
|
|
1492
1647
|
var startMs = _now();
|
|
1493
1648
|
var records;
|
|
1494
1649
|
try {
|
|
@@ -2107,6 +2262,13 @@ module.exports = {
|
|
|
2107
2262
|
nodeLookup: nodeLookup,
|
|
2108
2263
|
clearCache: _clearCache,
|
|
2109
2264
|
DnsError: DnsError,
|
|
2265
|
+
_encodeDnsQuery: _encodeDnsQuery,
|
|
2266
|
+
_validateHostShape: _validateHostShape,
|
|
2267
|
+
// Exported for the same reason as the shape check: it is the SECOND gate a
|
|
2268
|
+
// public query passes, and a name that clears one and not the other resolves
|
|
2269
|
+
// through some entry points and not others. Asserting it directly proves that
|
|
2270
|
+
// without a test having to reach the network.
|
|
2271
|
+
_validateLdh: _validateLdh,
|
|
2110
2272
|
_parseSvcbRdata: _parseSvcbRdata,
|
|
2111
2273
|
_decodeDnsAnswerRaw: _decodeDnsAnswerRaw,
|
|
2112
2274
|
_readDnsName: _readDnsName,
|
package/lib/public-suffix.js
CHANGED
|
@@ -74,6 +74,38 @@ function _err(code, message) {
|
|
|
74
74
|
return new PublicSuffixError(code, message);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// The index of the first character that cannot appear in a host name, or -1.
|
|
78
|
+
//
|
|
79
|
+
// Control / NUL / whitespace bytes, DEL, and the URL-structural delimiters
|
|
80
|
+
// domainToASCII silently TRUNCATES at — "/" (0x2F), "?" (0x3F), "#" (0x23),
|
|
81
|
+
// "\" (0x5C) reduce "example.com/evil" to "example.com" rather than failing,
|
|
82
|
+
// which would let a hostile host masquerade as a trusted prefix. ":" / "@" /
|
|
83
|
+
// "[" / "]" already make domainToASCII return "", but they are rejected here
|
|
84
|
+
// too so every non-host character fails closed rather than silently.
|
|
85
|
+
//
|
|
86
|
+
// Exported because it is the framework's definition of "a character that may
|
|
87
|
+
// appear in a host", and the DNS wire encoder needs the same answer. When it
|
|
88
|
+
// had its own — a shorter one — `b.network.dns` encoded `a\u0000.com` and
|
|
89
|
+
// `example.com/evil` into query labels that this module refuses outright.
|
|
90
|
+
function _firstNonHostCharacter(name) {
|
|
91
|
+
for (var i = 0; i < name.length; i += 1) {
|
|
92
|
+
var cp = name.charCodeAt(i);
|
|
93
|
+
if (cp < 0x21 || cp === 0x7f ||
|
|
94
|
+
cp === 0x2f || cp === 0x3f || cp === 0x23 || cp === 0x5c || // / ? # \
|
|
95
|
+
cp === 0x3a || cp === 0x40 || cp === 0x5b || cp === 0x5d) { // : @ [ ]
|
|
96
|
+
return i;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The characters UTS #46 treats as a label separator, and therefore the ones
|
|
103
|
+
// that can mark the root of an absolute name. Scanned against Node's own
|
|
104
|
+
// mapping and found to be exactly this set across the BMP and the SMP.
|
|
105
|
+
function _isRootMarker(ch) {
|
|
106
|
+
return ch === "." || ch === "。" || ch === "." || ch === "。";
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
// _normalizeInput — lowercase + IDN-normalize a candidate domain.
|
|
78
110
|
// Returns a plain ASCII (punycode) string with no leading/trailing
|
|
79
111
|
// dots and no empty labels. Throws PublicSuffixError on bad shape so
|
|
@@ -87,36 +119,41 @@ function _normalizeInput(domain) {
|
|
|
87
119
|
throw _err("public-suffix/invalid-domain",
|
|
88
120
|
"publicSuffix: domain must not be empty");
|
|
89
121
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
122
|
+
// Strip a single trailing root marker (FQDN form) BEFORE measuring. The
|
|
123
|
+
// absolute and relative spellings of one name encode to identical wire
|
|
124
|
+
// bytes, so measuring the marker would refuse a maximum-length name written
|
|
125
|
+
// absolutely while accepting it written relatively.
|
|
126
|
+
//
|
|
127
|
+
// Which character marks the root is not fixed: UTS #46 maps U+3002, U+FF0E
|
|
128
|
+
// and U+FF61 to "." as well, so `münchen.example。` is absolute too.
|
|
129
|
+
// Recognising all four here rather than only the ASCII dot is what lets the
|
|
130
|
+
// `rootStripped` guard below hold — exactly one marker comes off in total,
|
|
131
|
+
// and a second is an empty final label rather than something to remove.
|
|
132
|
+
//
|
|
133
|
+
// A marker that only APPEARS during conversion — because an IDNA-ignored
|
|
134
|
+
// code point trailed it — is handled by the post-conversion strip below,
|
|
135
|
+
// which the same guard keeps mutually exclusive with this one.
|
|
98
136
|
var s = domain.toLowerCase();
|
|
99
|
-
|
|
137
|
+
var rootStripped = false;
|
|
138
|
+
if (_isRootMarker(s.charAt(s.length - 1))) {
|
|
100
139
|
s = s.slice(0, -1);
|
|
140
|
+
rootStripped = true;
|
|
101
141
|
if (s.length === 0) {
|
|
102
142
|
throw _err("public-suffix/invalid-domain",
|
|
103
143
|
"publicSuffix: domain must not be a bare dot");
|
|
104
144
|
}
|
|
105
145
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
throw _err("public-suffix/invalid-domain",
|
|
118
|
-
"publicSuffix: domain contains a control byte or URL delimiter");
|
|
119
|
-
}
|
|
146
|
+
if (s.length > 253) {
|
|
147
|
+
// A cheap bound on the INPUT, so a pathological string is refused before
|
|
148
|
+
// conversion. It is not the authoritative check: an internationalized name
|
|
149
|
+
// grows when it becomes A-labels, so the real test is on the converted form
|
|
150
|
+
// below. Every ASCII name is settled here, since conversion leaves it as-is.
|
|
151
|
+
throw _err("public-suffix/invalid-domain",
|
|
152
|
+
"publicSuffix: domain exceeds 253-octet RFC 1035 limit");
|
|
153
|
+
}
|
|
154
|
+
if (_firstNonHostCharacter(s) !== -1) {
|
|
155
|
+
throw _err("public-suffix/invalid-domain",
|
|
156
|
+
"publicSuffix: domain contains a control byte or URL delimiter");
|
|
120
157
|
}
|
|
121
158
|
// IDN-normalize — non-ASCII labels become xn--… via Node's UTS #46
|
|
122
159
|
// implementation. Empty string back means the input was malformed
|
|
@@ -126,11 +163,55 @@ function _normalizeInput(domain) {
|
|
|
126
163
|
throw _err("public-suffix/invalid-domain",
|
|
127
164
|
"publicSuffix: domain failed IDN normalization");
|
|
128
165
|
}
|
|
129
|
-
// No empty labels (`foo..bar`) and no leading dot.
|
|
166
|
+
// No empty labels (`foo..bar`) and no leading dot. This runs BEFORE the
|
|
167
|
+
// root-marker strip below, so a name carrying an empty final label cannot be
|
|
168
|
+
// turned into a valid one by removing a dot: `münchen.example。。` converts to
|
|
169
|
+
// a trailing `..` and is refused here rather than quietly becoming a
|
|
170
|
+
// different, real domain.
|
|
130
171
|
if (ascii.indexOf("..") !== -1 || ascii.charCodeAt(0) === 46) {
|
|
131
172
|
throw _err("public-suffix/invalid-domain",
|
|
132
173
|
"publicSuffix: domain contains empty label");
|
|
133
174
|
}
|
|
175
|
+
// RFC 1035 §2.3.4 — 253 octets max for the wire form (255 minus the leading
|
|
176
|
+
// length byte and the root's null). This is the AUTHORITATIVE check, and it
|
|
177
|
+
// has to run on the converted name: an internationalized label grows into its
|
|
178
|
+
// `xn--` form, so five 44-character labels are 224 characters going in and
|
|
179
|
+
// 254 octets coming out, with every individual label a legal 50. Measuring
|
|
180
|
+
// the input handed the caller a name that cannot be put on the wire, and a
|
|
181
|
+
// caller cannot tell — it looks like any other domain, and the DMARC walk
|
|
182
|
+
// stepped over the unqueryable target and applied an ancestor's policy.
|
|
183
|
+
//
|
|
184
|
+
// A trailing root marker is still present at this point and does not count:
|
|
185
|
+
// the wire form carries the root as a zero-length label, not a character.
|
|
186
|
+
var withoutRoot = ascii.charCodeAt(ascii.length - 1) === 46
|
|
187
|
+
? ascii.length - 1 : ascii.length;
|
|
188
|
+
if (withoutRoot > 253) {
|
|
189
|
+
throw _err("public-suffix/invalid-domain",
|
|
190
|
+
"publicSuffix: domain exceeds 253-octet RFC 1035 limit once converted " +
|
|
191
|
+
"to A-labels (" + withoutRoot + " octets)");
|
|
192
|
+
}
|
|
193
|
+
// The root marker is stripped here when it was not an ASCII dot on the way
|
|
194
|
+
// in. UTS #46 maps U+3002, U+FF0E and U+FF61 to ".", so `münchen.example。`
|
|
195
|
+
// arrives with no trailing dot and leaves the conversion with one. Returning
|
|
196
|
+
// that from a function whose contract is to strip the trailing dot leaves
|
|
197
|
+
// every caller to compensate, and the ones that do not compare two spellings
|
|
198
|
+
// of the same absolute name as different names.
|
|
199
|
+
//
|
|
200
|
+
// At most ONE root marker is removed in total. A dot still here after one was
|
|
201
|
+
// already taken off means the name ended in two of them — an empty final
|
|
202
|
+
// label — and stripping the second would hand the caller a different, valid
|
|
203
|
+
// domain than the one they asked about.
|
|
204
|
+
if (ascii.charCodeAt(ascii.length - 1) === 46 /* "." */) {
|
|
205
|
+
if (rootStripped) {
|
|
206
|
+
throw _err("public-suffix/invalid-domain",
|
|
207
|
+
"publicSuffix: domain contains empty label");
|
|
208
|
+
}
|
|
209
|
+
ascii = ascii.slice(0, -1);
|
|
210
|
+
if (ascii.length === 0) {
|
|
211
|
+
throw _err("public-suffix/invalid-domain",
|
|
212
|
+
"publicSuffix: domain must not be a bare dot");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
134
215
|
return ascii;
|
|
135
216
|
}
|
|
136
217
|
|
|
@@ -462,4 +543,9 @@ module.exports = {
|
|
|
462
543
|
canonicalDomain: canonicalDomain,
|
|
463
544
|
isPublicSuffix: isPublicSuffix,
|
|
464
545
|
lookupSource: lookupSource,
|
|
546
|
+
_firstNonHostCharacter: _firstNonHostCharacter,
|
|
547
|
+
// Exported for the same reason as the character predicate above: this is the
|
|
548
|
+
// framework's answer to "which characters mark the root of an absolute name",
|
|
549
|
+
// and a second copy of the set drifts from it.
|
|
550
|
+
_isRootMarker: _isRootMarker,
|
|
465
551
|
};
|