@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/mail.js
CHANGED
|
@@ -248,10 +248,9 @@ async function reverseDns(ip) {
|
|
|
248
248
|
// the original input. RFC 8601 §3 says the forward query must use
|
|
249
249
|
// the same family as the source; mismatched families don't count
|
|
250
250
|
// as confirmation.
|
|
251
|
-
var net = require("node:net");
|
|
252
251
|
var forwardAddrs = [];
|
|
253
252
|
try {
|
|
254
|
-
if (net.isIPv6(ip)) {
|
|
253
|
+
if (net().isIPv6(ip)) {
|
|
255
254
|
forwardAddrs = await dns.resolveAaaa(ptrName);
|
|
256
255
|
} else {
|
|
257
256
|
forwardAddrs = await dns.resolve4(ptrName);
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
* }
|
|
70
70
|
*/
|
|
71
71
|
var C = require("../constants");
|
|
72
|
+
var cookies = require("../cookies");
|
|
72
73
|
var lazyRequire = require("../lazy-require");
|
|
73
74
|
var pick = require("../pick");
|
|
74
75
|
var forms = require("../forms");
|
|
@@ -130,33 +131,6 @@ function _parseCookieHeader(header) {
|
|
|
130
131
|
return Object.assign(Object.create(null), Object.fromEntries(pairs));
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
function _formatSetCookie(name, value, opts) {
|
|
134
|
-
var parts = [name + "=" + value];
|
|
135
|
-
parts.push("Path=" + (opts.path || "/"));
|
|
136
|
-
parts.push("SameSite=" + (opts.sameSite || "Lax"));
|
|
137
|
-
if (opts.httpOnly) parts.push("HttpOnly");
|
|
138
|
-
if (opts.secure) parts.push("Secure");
|
|
139
|
-
if (opts.maxAge != null) parts.push("Max-Age=" + opts.maxAge);
|
|
140
|
-
return parts.join("; ");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function _appendSetCookie(res, value) {
|
|
144
|
-
// Don't clobber other Set-Cookie headers the route may have already
|
|
145
|
-
// queued (login session cookie, etc.). Use res.appendHeader when
|
|
146
|
-
// available; else array-merge manually.
|
|
147
|
-
if (typeof res.appendHeader === "function") {
|
|
148
|
-
res.appendHeader("Set-Cookie", value);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
var existing = typeof res.getHeader === "function" ? res.getHeader("Set-Cookie") : undefined;
|
|
152
|
-
if (existing == null) {
|
|
153
|
-
res.setHeader("Set-Cookie", value);
|
|
154
|
-
} else if (Array.isArray(existing)) {
|
|
155
|
-
res.setHeader("Set-Cookie", existing.concat(value));
|
|
156
|
-
} else {
|
|
157
|
-
res.setHeader("Set-Cookie", [existing, value]);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
134
|
|
|
161
135
|
// csrf-protect does NOT buffer or parse the request body itself.
|
|
162
136
|
// Operators who use form-urlencoded POSTs MUST register
|
|
@@ -431,30 +405,44 @@ function create(opts) {
|
|
|
431
405
|
if (["Lax", "Strict", "None"].indexOf(cookieCfg.sameSite) === -1) {
|
|
432
406
|
throw new Error("middleware.csrfProtect: opts.cookie.sameSite must be Lax|Strict|None");
|
|
433
407
|
}
|
|
434
|
-
// Cookie name-prefix safety (RFC 6265bis §4.1.3).
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
// `
|
|
441
|
-
//
|
|
442
|
-
//
|
|
408
|
+
// Cookie name-prefix safety (RFC 6265bis §4.1.3). b.cookies.serialize
|
|
409
|
+
// enforces the same invariants when the header is built, but that is a
|
|
410
|
+
// per-request throw inside the middleware; catching the bad configuration
|
|
411
|
+
// at boot turns it into a startup error the operator can read. §5.4
|
|
412
|
+
// requires user agents to apply the prefix test case-INSENSITIVELY (the
|
|
413
|
+
// server-side §4.1.3 description reads "case-sensitive", but the UA is what
|
|
414
|
+
// drops the cookie), so `__host-`/`__SECURE-` get the same browser
|
|
415
|
+
// enforcement as `__Host-`/`__Secure-` -- case-sensitive matching was itself
|
|
416
|
+
// CVE-2024-5699. Compare a lowercased copy so a case-variant name can't
|
|
417
|
+
// dodge the invariant here and then be silently rejected by the browser.
|
|
443
418
|
// __Host-* — Path must be "/", no Domain (we never set one), Secure.
|
|
444
419
|
// __Secure-* — Secure.
|
|
445
420
|
if (cookieCfg.name) {
|
|
446
421
|
var lowerCookieName = cookieCfg.name.toLowerCase();
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
422
|
+
var isHostPrefix = lowerCookieName.indexOf("__host-") === 0;
|
|
423
|
+
var isSecurePrefix = lowerCookieName.indexOf("__secure-") === 0;
|
|
424
|
+
if (isHostPrefix && cookieCfg.path !== "/") {
|
|
425
|
+
throw new Error("middleware.csrfProtect: __Host-* cookie name requires path='/'");
|
|
426
|
+
}
|
|
427
|
+
if (isHostPrefix && cookieCfg.secure === false) {
|
|
428
|
+
throw new Error("middleware.csrfProtect: __Host-* cookie name requires secure (cannot be explicit false)");
|
|
429
|
+
}
|
|
430
|
+
if (isSecurePrefix && cookieCfg.secure === false) {
|
|
431
|
+
throw new Error("middleware.csrfProtect: __Secure-* cookie name requires secure (cannot be explicit false)");
|
|
432
|
+
}
|
|
433
|
+
// A prefixed name is a promise to the browser that the cookie is always
|
|
434
|
+
// Secure. Leaving `secure` to per-request auto-detection breaks that
|
|
435
|
+
// promise on any cleartext request: the cookie goes out prefixed and
|
|
436
|
+
// WITHOUT Secure, the browser drops it, and the double-submit token
|
|
437
|
+
// silently never persists. The name and the auto-detect cannot both
|
|
438
|
+
// stand — the operator picking the prefix is the one asserting HTTPS,
|
|
439
|
+
// so make them say it.
|
|
440
|
+
if ((isHostPrefix || isSecurePrefix) && cookieCfg.secure == null) {
|
|
441
|
+
throw new Error("middleware.csrfProtect: " +
|
|
442
|
+
(isHostPrefix ? "__Host-*" : "__Secure-*") +
|
|
443
|
+
" cookie name requires an explicit cookie.secure: true — " +
|
|
444
|
+
"auto-detected secure emits the prefix without Secure on a plain-HTTP " +
|
|
445
|
+
"request, which browsers reject");
|
|
458
446
|
}
|
|
459
447
|
}
|
|
460
448
|
}
|
|
@@ -485,8 +473,8 @@ function create(opts) {
|
|
|
485
473
|
function _issueIfNeeded(req, res) {
|
|
486
474
|
if (!cookieCfg) return null;
|
|
487
475
|
var cookieName = _resolveCookieName(req);
|
|
488
|
-
var
|
|
489
|
-
var existing =
|
|
476
|
+
var requestCookies = _parseCookieHeader(req.headers && req.headers.cookie);
|
|
477
|
+
var existing = requestCookies[cookieName];
|
|
490
478
|
// Strict 64-hex-char check matches the byte-length of every token
|
|
491
479
|
// forms.generateCsrfToken() produces (CSRF_TOKEN_BYTES = 32 bytes
|
|
492
480
|
// → 64 hex chars). The previous {2,} floor accepted any 2-char
|
|
@@ -522,14 +510,20 @@ function create(opts) {
|
|
|
522
510
|
} catch (_e) { /* drop-silent */ }
|
|
523
511
|
}
|
|
524
512
|
var fresh = forms.generateCsrfToken();
|
|
525
|
-
|
|
513
|
+
// b.cookies owns Set-Cookie: it validates the name as an RFC 6265 token,
|
|
514
|
+
// refuses CRLF / NUL in the value, scrubs the Path and Domain attributes
|
|
515
|
+
// before they reach a response header, and enforces the RFC 6265bis
|
|
516
|
+
// prefix invariants. The token is 64 hex characters, so the percent-
|
|
517
|
+
// encoding serialize applies to the value is a no-op on it and the cookie
|
|
518
|
+
// the browser echoes back still matches the double-submit compare.
|
|
519
|
+
var setCookie = cookies.serialize(cookieName, fresh, {
|
|
526
520
|
path: cookieCfg.path,
|
|
527
521
|
sameSite: cookieCfg.sameSite,
|
|
528
522
|
secure: cookieCfg.secure == null ? _isHttps(req) : !!cookieCfg.secure,
|
|
529
523
|
httpOnly: cookieCfg.httpOnly,
|
|
530
524
|
maxAge: cookieCfg.maxAge,
|
|
531
525
|
});
|
|
532
|
-
|
|
526
|
+
cookies.appendSetCookie(res, setCookie);
|
|
533
527
|
req._csrfIssuedCookies[cookieName] = fresh;
|
|
534
528
|
req.csrfToken = fresh;
|
|
535
529
|
return fresh;
|
package/lib/migrations.js
CHANGED
|
@@ -297,12 +297,12 @@ function create(opts) {
|
|
|
297
297
|
var dir = opts.dir;
|
|
298
298
|
|
|
299
299
|
function _appliedRows() {
|
|
300
|
-
var
|
|
301
|
-
_ensureTable(
|
|
302
|
-
var q = sql.select(_migrationsTable(), _sqlOpts(
|
|
300
|
+
var conn = _resolveDb(opts);
|
|
301
|
+
_ensureTable(conn);
|
|
302
|
+
var q = sql.select(_migrationsTable(), _sqlOpts(conn))
|
|
303
303
|
.columns(["name", "description", "appliedAt"])
|
|
304
304
|
.orderBy("appliedAt", "asc").orderBy("name", "asc").toSql();
|
|
305
|
-
var stmt =
|
|
305
|
+
var stmt = conn.prepare(q.sql);
|
|
306
306
|
return stmt.all.apply(stmt, q.params);
|
|
307
307
|
}
|
|
308
308
|
|
|
@@ -319,11 +319,11 @@ function create(opts) {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
function up() {
|
|
322
|
-
var
|
|
323
|
-
_ensureTable(
|
|
324
|
-
return _withLock(
|
|
325
|
-
var namesQ = sql.select(_migrationsTable(), _sqlOpts(
|
|
326
|
-
var namesStmt =
|
|
322
|
+
var conn = _resolveDb(opts);
|
|
323
|
+
_ensureTable(conn);
|
|
324
|
+
return _withLock(conn, opts, function () {
|
|
325
|
+
var namesQ = sql.select(_migrationsTable(), _sqlOpts(conn)).columns(["name"]).toSql();
|
|
326
|
+
var namesStmt = conn.prepare(namesQ.sql);
|
|
327
327
|
var appliedSet = new Set(
|
|
328
328
|
namesStmt.all.apply(namesStmt, namesQ.params)
|
|
329
329
|
.map(function (r) { return r.name; })
|
|
@@ -336,12 +336,12 @@ function create(opts) {
|
|
|
336
336
|
if (appliedSet.has(file)) { skipped.push(file); continue; }
|
|
337
337
|
var mod = _loadMigration(file, dir);
|
|
338
338
|
try {
|
|
339
|
-
_txn(
|
|
340
|
-
mod.up(
|
|
341
|
-
var insQ = sql.insert(_migrationsTable(), _sqlOpts(
|
|
339
|
+
_txn(conn, function () {
|
|
340
|
+
mod.up(conn);
|
|
341
|
+
var insQ = sql.insert(_migrationsTable(), _sqlOpts(conn))
|
|
342
342
|
.values({ name: file, description: mod.description || "",
|
|
343
343
|
appliedAt: new Date().toISOString() }).toSql();
|
|
344
|
-
var insStmt =
|
|
344
|
+
var insStmt = conn.prepare(insQ.sql);
|
|
345
345
|
insStmt.run.apply(insStmt, insQ.params);
|
|
346
346
|
});
|
|
347
347
|
} catch (e) {
|
|
@@ -363,16 +363,16 @@ function create(opts) {
|
|
|
363
363
|
"down: steps must be a positive integer (got " + opts2.steps + ")",
|
|
364
364
|
true);
|
|
365
365
|
}
|
|
366
|
-
var
|
|
367
|
-
_ensureTable(
|
|
368
|
-
return _withLock(
|
|
366
|
+
var conn = _resolveDb(opts);
|
|
367
|
+
_ensureTable(conn);
|
|
368
|
+
return _withLock(conn, opts, function () {
|
|
369
369
|
// Most-recent applied first (reverse chronological by appliedAt
|
|
370
370
|
// then by name as a stable tiebreaker for fixtures with identical
|
|
371
371
|
// timestamps). steps is a validated positive integer, so b.sql
|
|
372
372
|
// inlines the LIMIT.
|
|
373
|
-
var downQ = sql.select(_migrationsTable(), _sqlOpts(
|
|
373
|
+
var downQ = sql.select(_migrationsTable(), _sqlOpts(conn)).columns(["name"])
|
|
374
374
|
.orderBy("appliedAt", "desc").orderBy("name", "desc").limit(steps).toSql();
|
|
375
|
-
var downStmt =
|
|
375
|
+
var downStmt = conn.prepare(downQ.sql);
|
|
376
376
|
var rows = downStmt.all.apply(downStmt, downQ.params);
|
|
377
377
|
|
|
378
378
|
var reverted = [];
|
|
@@ -386,10 +386,10 @@ function create(opts) {
|
|
|
386
386
|
true);
|
|
387
387
|
}
|
|
388
388
|
try {
|
|
389
|
-
_txn(
|
|
390
|
-
mod.down(
|
|
391
|
-
var delQ = sql.delete(_migrationsTable(), _sqlOpts(
|
|
392
|
-
var delStmt =
|
|
389
|
+
_txn(conn, function () {
|
|
390
|
+
mod.down(conn);
|
|
391
|
+
var delQ = sql.delete(_migrationsTable(), _sqlOpts(conn)).where("name", file).toSql();
|
|
392
|
+
var delStmt = conn.prepare(delQ.sql);
|
|
393
393
|
delStmt.run.apply(delStmt, delQ.params);
|
|
394
394
|
});
|
|
395
395
|
} catch (e) {
|
|
@@ -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,
|