@fedify/vocab-runtime 2.2.7 → 2.2.8
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/deno.json +1 -1
- package/dist/mod.cjs +33 -6
- package/dist/mod.d.cts +3 -1
- package/dist/mod.d.ts +3 -2
- package/dist/mod.js +33 -6
- package/dist/tests/decimal.test.cjs +2 -2
- package/dist/tests/decimal.test.mjs +2 -2
- package/dist/tests/{docloader-B4HK1gHZ.mjs → docloader-CX1mRgBh.mjs} +2 -2
- package/dist/tests/{docloader-DwVt5hGF.cjs → docloader-yj9VGANT.cjs} +2 -2
- package/dist/tests/docloader.test.cjs +3 -3
- package/dist/tests/docloader.test.mjs +3 -3
- package/dist/tests/{request-KYO06U6S.cjs → request-BOX7nq1q.cjs} +1 -1
- package/dist/tests/{request-JJ34PA3H.mjs → request-CoxDPWoc.mjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-Ds8K8vxN.cjs → url-CWU5HT3Y.cjs} +38 -5
- package/dist/tests/{url-m9Qzxy-Y.mjs → url-o0e-aXQY.mjs} +33 -6
- package/dist/tests/url.test.cjs +30 -1
- package/dist/tests/url.test.mjs +31 -2
- package/package.json +1 -1
- package/src/url.test.ts +28 -1
- package/src/url.ts +35 -5
package/deno.json
CHANGED
package/dist/mod.cjs
CHANGED
|
@@ -4384,7 +4384,7 @@ const preloadedContexts = {
|
|
|
4384
4384
|
//#endregion
|
|
4385
4385
|
//#region deno.json
|
|
4386
4386
|
var name = "@fedify/vocab-runtime";
|
|
4387
|
-
var version = "2.2.
|
|
4387
|
+
var version = "2.2.8";
|
|
4388
4388
|
//#endregion
|
|
4389
4389
|
//#region src/link.ts
|
|
4390
4390
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4639,8 +4639,8 @@ function logRequest(logger, request) {
|
|
|
4639
4639
|
//#endregion
|
|
4640
4640
|
//#region src/url.ts
|
|
4641
4641
|
var UrlError = class extends Error {
|
|
4642
|
-
constructor(message) {
|
|
4643
|
-
super(message);
|
|
4642
|
+
constructor(message, options) {
|
|
4643
|
+
super(message, options);
|
|
4644
4644
|
this.name = "UrlError";
|
|
4645
4645
|
}
|
|
4646
4646
|
};
|
|
@@ -4668,10 +4668,37 @@ async function validatePublicUrl(url) {
|
|
|
4668
4668
|
let addresses;
|
|
4669
4669
|
try {
|
|
4670
4670
|
addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
|
|
4671
|
-
} catch {
|
|
4672
|
-
|
|
4671
|
+
} catch (error) {
|
|
4672
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
4673
4673
|
}
|
|
4674
|
-
|
|
4674
|
+
validateLookupAddresses(addresses);
|
|
4675
|
+
}
|
|
4676
|
+
/**
|
|
4677
|
+
* Validates the IP addresses returned by `node:dns.lookup()`.
|
|
4678
|
+
*
|
|
4679
|
+
* Cloudflare Workers' `node:dns` implementation currently maps every record
|
|
4680
|
+
* in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
|
|
4681
|
+
* `LookupAddress`, even though Node.js specifies that the `address` field must
|
|
4682
|
+
* contain an IPv4 or IPv6 literal. See:
|
|
4683
|
+
* https://github.com/cloudflare/workerd/issues/6886
|
|
4684
|
+
*
|
|
4685
|
+
* Work around that bug by ignoring non-IP entries only when the lookup also
|
|
4686
|
+
* returns at least one actual IP address. This remains fail-closed: a result
|
|
4687
|
+
* containing no IP addresses is rejected, and every returned IP address is
|
|
4688
|
+
* still validated and must be public. This workaround can be revisited once
|
|
4689
|
+
* the Workerd issue is fixed in supported Cloudflare Workers runtimes.
|
|
4690
|
+
*
|
|
4691
|
+
* @internal
|
|
4692
|
+
*/
|
|
4693
|
+
function validateLookupAddresses(addresses) {
|
|
4694
|
+
let ipAddressCount = 0;
|
|
4695
|
+
for (const { address } of addresses) {
|
|
4696
|
+
const family = (0, node_net.isIP)(address);
|
|
4697
|
+
if (family === 0) continue;
|
|
4698
|
+
ipAddressCount++;
|
|
4699
|
+
validatePublicIpAddress(address, family);
|
|
4700
|
+
}
|
|
4701
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
4675
4702
|
}
|
|
4676
4703
|
function validatePublicIpAddress(address, family) {
|
|
4677
4704
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
package/dist/mod.d.cts
CHANGED
|
@@ -424,7 +424,9 @@ declare function encodingFromBaseData(data: string | Uint8Array): Base;
|
|
|
424
424
|
//#endregion
|
|
425
425
|
//#region src/url.d.ts
|
|
426
426
|
declare class UrlError extends Error {
|
|
427
|
-
constructor(message: string
|
|
427
|
+
constructor(message: string, options?: {
|
|
428
|
+
cause?: unknown;
|
|
429
|
+
});
|
|
428
430
|
}
|
|
429
431
|
/**
|
|
430
432
|
* Validates a URL to prevent SSRF attacks.
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
2
|
import { Logger } from "@logtape/logtape";
|
|
3
|
-
|
|
4
3
|
//#region src/contexts.d.ts
|
|
5
4
|
declare const preloadedContexts: Record<string, unknown>;
|
|
6
5
|
//#endregion
|
|
@@ -424,7 +423,9 @@ declare function encodingFromBaseData(data: string | Uint8Array): Base;
|
|
|
424
423
|
//#endregion
|
|
425
424
|
//#region src/url.d.ts
|
|
426
425
|
declare class UrlError extends Error {
|
|
427
|
-
constructor(message: string
|
|
426
|
+
constructor(message: string, options?: {
|
|
427
|
+
cause?: unknown;
|
|
428
|
+
});
|
|
428
429
|
}
|
|
429
430
|
/**
|
|
430
431
|
* Validates a URL to prevent SSRF attacks.
|
package/dist/mod.js
CHANGED
|
@@ -4380,7 +4380,7 @@ const preloadedContexts = {
|
|
|
4380
4380
|
//#endregion
|
|
4381
4381
|
//#region deno.json
|
|
4382
4382
|
var name = "@fedify/vocab-runtime";
|
|
4383
|
-
var version = "2.2.
|
|
4383
|
+
var version = "2.2.8";
|
|
4384
4384
|
//#endregion
|
|
4385
4385
|
//#region src/link.ts
|
|
4386
4386
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4635,8 +4635,8 @@ function logRequest(logger, request) {
|
|
|
4635
4635
|
//#endregion
|
|
4636
4636
|
//#region src/url.ts
|
|
4637
4637
|
var UrlError = class extends Error {
|
|
4638
|
-
constructor(message) {
|
|
4639
|
-
super(message);
|
|
4638
|
+
constructor(message, options) {
|
|
4639
|
+
super(message, options);
|
|
4640
4640
|
this.name = "UrlError";
|
|
4641
4641
|
}
|
|
4642
4642
|
};
|
|
@@ -4664,10 +4664,37 @@ async function validatePublicUrl(url) {
|
|
|
4664
4664
|
let addresses;
|
|
4665
4665
|
try {
|
|
4666
4666
|
addresses = await lookup(hostname, { all: true });
|
|
4667
|
-
} catch {
|
|
4668
|
-
|
|
4667
|
+
} catch (error) {
|
|
4668
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
4669
4669
|
}
|
|
4670
|
-
|
|
4670
|
+
validateLookupAddresses(addresses);
|
|
4671
|
+
}
|
|
4672
|
+
/**
|
|
4673
|
+
* Validates the IP addresses returned by `node:dns.lookup()`.
|
|
4674
|
+
*
|
|
4675
|
+
* Cloudflare Workers' `node:dns` implementation currently maps every record
|
|
4676
|
+
* in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
|
|
4677
|
+
* `LookupAddress`, even though Node.js specifies that the `address` field must
|
|
4678
|
+
* contain an IPv4 or IPv6 literal. See:
|
|
4679
|
+
* https://github.com/cloudflare/workerd/issues/6886
|
|
4680
|
+
*
|
|
4681
|
+
* Work around that bug by ignoring non-IP entries only when the lookup also
|
|
4682
|
+
* returns at least one actual IP address. This remains fail-closed: a result
|
|
4683
|
+
* containing no IP addresses is rejected, and every returned IP address is
|
|
4684
|
+
* still validated and must be public. This workaround can be revisited once
|
|
4685
|
+
* the Workerd issue is fixed in supported Cloudflare Workers runtimes.
|
|
4686
|
+
*
|
|
4687
|
+
* @internal
|
|
4688
|
+
*/
|
|
4689
|
+
function validateLookupAddresses(addresses) {
|
|
4690
|
+
let ipAddressCount = 0;
|
|
4691
|
+
for (const { address } of addresses) {
|
|
4692
|
+
const family = isIP(address);
|
|
4693
|
+
if (family === 0) continue;
|
|
4694
|
+
ipAddressCount++;
|
|
4695
|
+
validatePublicIpAddress(address, family);
|
|
4696
|
+
}
|
|
4697
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
4671
4698
|
}
|
|
4672
4699
|
function validatePublicIpAddress(address, family) {
|
|
4673
4700
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require("./chunk-Do9eywBl.cjs");
|
|
2
|
-
require("./docloader-
|
|
3
|
-
require("./url-
|
|
2
|
+
require("./docloader-yj9VGANT.cjs");
|
|
3
|
+
require("./url-CWU5HT3Y.cjs");
|
|
4
4
|
require("./key-DTTIntwb.cjs");
|
|
5
5
|
require("./multibase-F7LtMMsK.cjs");
|
|
6
6
|
require("./langstr-CbAxaeEZ.cjs");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-
|
|
1
|
+
import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-CoxDPWoc.mjs";
|
|
2
2
|
import { t as HttpHeaderLink } from "./link-NUUWCdnK.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { o as validatePublicUrl, t as UrlError } from "./url-o0e-aXQY.mjs";
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
5
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
6
6
|
//#region src/contexts/activitystreams.json
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require("./chunk-Do9eywBl.cjs");
|
|
2
|
-
const require_request = require("./request-
|
|
2
|
+
const require_request = require("./request-BOX7nq1q.cjs");
|
|
3
3
|
const require_link = require("./link-FguCydMA.cjs");
|
|
4
|
-
const require_url = require("./url-
|
|
4
|
+
const require_url = require("./url-CWU5HT3Y.cjs");
|
|
5
5
|
let _logtape_logtape = require("@logtape/logtape");
|
|
6
6
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
7
7
|
//#region src/contexts/activitystreams.json
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Do9eywBl.cjs");
|
|
2
|
-
const require_docloader = require("./docloader-
|
|
3
|
-
const require_request = require("./request-
|
|
4
|
-
const require_url = require("./url-
|
|
2
|
+
const require_docloader = require("./docloader-yj9VGANT.cjs");
|
|
3
|
+
const require_request = require("./request-BOX7nq1q.cjs");
|
|
4
|
+
const require_url = require("./url-CWU5HT3Y.cjs");
|
|
5
5
|
let node_assert = require("node:assert");
|
|
6
6
|
let node_test = require("node:test");
|
|
7
7
|
//#region ../../node_modules/.pnpm/glob-to-regexp@0.4.1/node_modules/glob-to-regexp/index.js
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as preloadedContexts, t as getDocumentLoader } from "./docloader-
|
|
2
|
-
import { t as FetchError } from "./request-
|
|
3
|
-
import { t as UrlError } from "./url-
|
|
1
|
+
import { n as preloadedContexts, t as getDocumentLoader } from "./docloader-CX1mRgBh.mjs";
|
|
2
|
+
import { t as FetchError } from "./request-CoxDPWoc.mjs";
|
|
3
|
+
import { t as UrlError } from "./url-o0e-aXQY.mjs";
|
|
4
4
|
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
5
5
|
import { test } from "node:test";
|
|
6
6
|
//#region \0rolldown/runtime.js
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Do9eywBl.cjs");
|
|
2
|
-
const require_request = require("./request-
|
|
2
|
+
const require_request = require("./request-BOX7nq1q.cjs");
|
|
3
3
|
let node_assert = require("node:assert");
|
|
4
4
|
let node_test = require("node:test");
|
|
5
5
|
let node_process = require("node:process");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as version, r as getUserAgent } from "./request-
|
|
1
|
+
import { o as version, r as getUserAgent } from "./request-CoxDPWoc.mjs";
|
|
2
2
|
import { deepStrictEqual } from "node:assert";
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
import process from "node:process";
|
|
@@ -3,8 +3,8 @@ let node_dns_promises = require("node:dns/promises");
|
|
|
3
3
|
let node_net = require("node:net");
|
|
4
4
|
//#region src/url.ts
|
|
5
5
|
var UrlError = class extends Error {
|
|
6
|
-
constructor(message) {
|
|
7
|
-
super(message);
|
|
6
|
+
constructor(message, options) {
|
|
7
|
+
super(message, options);
|
|
8
8
|
this.name = "UrlError";
|
|
9
9
|
}
|
|
10
10
|
};
|
|
@@ -32,10 +32,37 @@ async function validatePublicUrl(url) {
|
|
|
32
32
|
let addresses;
|
|
33
33
|
try {
|
|
34
34
|
addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
|
|
35
|
-
} catch {
|
|
36
|
-
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
validateLookupAddresses(addresses);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validates the IP addresses returned by `node:dns.lookup()`.
|
|
42
|
+
*
|
|
43
|
+
* Cloudflare Workers' `node:dns` implementation currently maps every record
|
|
44
|
+
* in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
|
|
45
|
+
* `LookupAddress`, even though Node.js specifies that the `address` field must
|
|
46
|
+
* contain an IPv4 or IPv6 literal. See:
|
|
47
|
+
* https://github.com/cloudflare/workerd/issues/6886
|
|
48
|
+
*
|
|
49
|
+
* Work around that bug by ignoring non-IP entries only when the lookup also
|
|
50
|
+
* returns at least one actual IP address. This remains fail-closed: a result
|
|
51
|
+
* containing no IP addresses is rejected, and every returned IP address is
|
|
52
|
+
* still validated and must be public. This workaround can be revisited once
|
|
53
|
+
* the Workerd issue is fixed in supported Cloudflare Workers runtimes.
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
function validateLookupAddresses(addresses) {
|
|
58
|
+
let ipAddressCount = 0;
|
|
59
|
+
for (const { address } of addresses) {
|
|
60
|
+
const family = (0, node_net.isIP)(address);
|
|
61
|
+
if (family === 0) continue;
|
|
62
|
+
ipAddressCount++;
|
|
63
|
+
validatePublicIpAddress(address, family);
|
|
64
|
+
}
|
|
65
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
39
66
|
}
|
|
40
67
|
function validatePublicIpAddress(address, family) {
|
|
41
68
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
|
@@ -198,6 +225,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
|
|
|
198
225
|
return isValidPublicIPv6Address;
|
|
199
226
|
}
|
|
200
227
|
});
|
|
228
|
+
Object.defineProperty(exports, "validateLookupAddresses", {
|
|
229
|
+
enumerable: true,
|
|
230
|
+
get: function() {
|
|
231
|
+
return validateLookupAddresses;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
201
234
|
Object.defineProperty(exports, "validatePublicUrl", {
|
|
202
235
|
enumerable: true,
|
|
203
236
|
get: function() {
|
|
@@ -2,8 +2,8 @@ import { lookup } from "node:dns/promises";
|
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
3
|
//#region src/url.ts
|
|
4
4
|
var UrlError = class extends Error {
|
|
5
|
-
constructor(message) {
|
|
6
|
-
super(message);
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, options);
|
|
7
7
|
this.name = "UrlError";
|
|
8
8
|
}
|
|
9
9
|
};
|
|
@@ -31,10 +31,37 @@ async function validatePublicUrl(url) {
|
|
|
31
31
|
let addresses;
|
|
32
32
|
try {
|
|
33
33
|
addresses = await lookup(hostname, { all: true });
|
|
34
|
-
} catch {
|
|
35
|
-
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
validateLookupAddresses(addresses);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validates the IP addresses returned by `node:dns.lookup()`.
|
|
41
|
+
*
|
|
42
|
+
* Cloudflare Workers' `node:dns` implementation currently maps every record
|
|
43
|
+
* in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
|
|
44
|
+
* `LookupAddress`, even though Node.js specifies that the `address` field must
|
|
45
|
+
* contain an IPv4 or IPv6 literal. See:
|
|
46
|
+
* https://github.com/cloudflare/workerd/issues/6886
|
|
47
|
+
*
|
|
48
|
+
* Work around that bug by ignoring non-IP entries only when the lookup also
|
|
49
|
+
* returns at least one actual IP address. This remains fail-closed: a result
|
|
50
|
+
* containing no IP addresses is rejected, and every returned IP address is
|
|
51
|
+
* still validated and must be public. This workaround can be revisited once
|
|
52
|
+
* the Workerd issue is fixed in supported Cloudflare Workers runtimes.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
function validateLookupAddresses(addresses) {
|
|
57
|
+
let ipAddressCount = 0;
|
|
58
|
+
for (const { address } of addresses) {
|
|
59
|
+
const family = isIP(address);
|
|
60
|
+
if (family === 0) continue;
|
|
61
|
+
ipAddressCount++;
|
|
62
|
+
validatePublicIpAddress(address, family);
|
|
63
|
+
}
|
|
64
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
38
65
|
}
|
|
39
66
|
function validatePublicIpAddress(address, family) {
|
|
40
67
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
|
@@ -173,4 +200,4 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
|
|
|
173
200
|
return true;
|
|
174
201
|
}
|
|
175
202
|
//#endregion
|
|
176
|
-
export {
|
|
203
|
+
export { validateLookupAddresses as a, isValidPublicIPv6Address as i, expandIPv6Address as n, validatePublicUrl as o, isValidPublicIPv4Address as r, UrlError as t };
|
package/dist/tests/url.test.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require("./chunk-Do9eywBl.cjs");
|
|
2
|
-
const require_url = require("./url-
|
|
2
|
+
const require_url = require("./url-CWU5HT3Y.cjs");
|
|
3
3
|
let node_assert = require("node:assert");
|
|
4
4
|
let node_test = require("node:test");
|
|
5
5
|
//#region src/url.test.ts
|
|
@@ -29,6 +29,35 @@ let node_test = require("node:test");
|
|
|
29
29
|
await require_url.validatePublicUrl("https://[2001:db8::1]");
|
|
30
30
|
await require_url.validatePublicUrl("https://[64:ff9b::8.8.8.8]");
|
|
31
31
|
});
|
|
32
|
+
(0, node_test.test)("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
|
|
33
|
+
require_url.validateLookupAddresses([
|
|
34
|
+
{
|
|
35
|
+
address: "app-host.example.net.",
|
|
36
|
+
family: 4
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
address: "93.184.216.34",
|
|
40
|
+
family: 4
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
address: "2606:2800:220:1:248:1893:25c8:1946",
|
|
44
|
+
family: 6
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
(0, node_test.test)("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
|
|
49
|
+
(0, node_assert.throws)(() => require_url.validateLookupAddresses([{
|
|
50
|
+
address: "private-host.example.net.",
|
|
51
|
+
family: 4
|
|
52
|
+
}, {
|
|
53
|
+
address: "127.0.0.1",
|
|
54
|
+
family: 4
|
|
55
|
+
}]), require_url.UrlError);
|
|
56
|
+
(0, node_assert.throws)(() => require_url.validateLookupAddresses([{
|
|
57
|
+
address: "app-host.example.net.",
|
|
58
|
+
family: 4
|
|
59
|
+
}]), require_url.UrlError);
|
|
60
|
+
});
|
|
32
61
|
(0, node_test.test)("isValidPublicIPv4Address()", () => {
|
|
33
62
|
(0, node_assert.ok)(require_url.isValidPublicIPv4Address("8.8.8.8"));
|
|
34
63
|
(0, node_assert.ok)(!require_url.isValidPublicIPv4Address("192.168.1.1"));
|
package/dist/tests/url.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
1
|
+
import { a as validateLookupAddresses, i as isValidPublicIPv6Address, n as expandIPv6Address, o as validatePublicUrl, r as isValidPublicIPv4Address, t as UrlError } from "./url-o0e-aXQY.mjs";
|
|
2
|
+
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
//#region src/url.test.ts
|
|
5
5
|
test("validatePublicUrl()", async () => {
|
|
@@ -28,6 +28,35 @@ test("validatePublicUrl()", async () => {
|
|
|
28
28
|
await validatePublicUrl("https://[2001:db8::1]");
|
|
29
29
|
await validatePublicUrl("https://[64:ff9b::8.8.8.8]");
|
|
30
30
|
});
|
|
31
|
+
test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
|
|
32
|
+
validateLookupAddresses([
|
|
33
|
+
{
|
|
34
|
+
address: "app-host.example.net.",
|
|
35
|
+
family: 4
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
address: "93.184.216.34",
|
|
39
|
+
family: 4
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
address: "2606:2800:220:1:248:1893:25c8:1946",
|
|
43
|
+
family: 6
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
|
|
48
|
+
throws(() => validateLookupAddresses([{
|
|
49
|
+
address: "private-host.example.net.",
|
|
50
|
+
family: 4
|
|
51
|
+
}, {
|
|
52
|
+
address: "127.0.0.1",
|
|
53
|
+
family: 4
|
|
54
|
+
}]), UrlError);
|
|
55
|
+
throws(() => validateLookupAddresses([{
|
|
56
|
+
address: "app-host.example.net.",
|
|
57
|
+
family: 4
|
|
58
|
+
}]), UrlError);
|
|
59
|
+
});
|
|
31
60
|
test("isValidPublicIPv4Address()", () => {
|
|
32
61
|
ok(isValidPublicIPv4Address("8.8.8.8"));
|
|
33
62
|
ok(!isValidPublicIPv4Address("192.168.1.1"));
|
package/package.json
CHANGED
package/src/url.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
1
|
+
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
expandIPv6Address,
|
|
5
5
|
isValidPublicIPv4Address,
|
|
6
6
|
isValidPublicIPv6Address,
|
|
7
7
|
UrlError,
|
|
8
|
+
validateLookupAddresses,
|
|
8
9
|
validatePublicUrl,
|
|
9
10
|
} from "./url.ts";
|
|
10
11
|
|
|
@@ -65,6 +66,32 @@ test("validatePublicUrl()", async () => {
|
|
|
65
66
|
await validatePublicUrl("https://[64:ff9b::8.8.8.8]");
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => {
|
|
70
|
+
validateLookupAddresses([
|
|
71
|
+
{ address: "app-host.example.net.", family: 4 },
|
|
72
|
+
{ address: "93.184.216.34", family: 4 },
|
|
73
|
+
{ address: "2606:2800:220:1:248:1893:25c8:1946", family: 6 },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => {
|
|
78
|
+
throws(
|
|
79
|
+
() =>
|
|
80
|
+
validateLookupAddresses([
|
|
81
|
+
{ address: "private-host.example.net.", family: 4 },
|
|
82
|
+
{ address: "127.0.0.1", family: 4 },
|
|
83
|
+
]),
|
|
84
|
+
UrlError,
|
|
85
|
+
);
|
|
86
|
+
throws(
|
|
87
|
+
() =>
|
|
88
|
+
validateLookupAddresses([
|
|
89
|
+
{ address: "app-host.example.net.", family: 4 },
|
|
90
|
+
]),
|
|
91
|
+
UrlError,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
68
95
|
test("isValidPublicIPv4Address()", () => {
|
|
69
96
|
ok(isValidPublicIPv4Address("8.8.8.8")); // Google DNS
|
|
70
97
|
ok(!isValidPublicIPv4Address("192.168.1.1")); // private
|
package/src/url.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { lookup } from "node:dns/promises";
|
|
|
3
3
|
import { isIP } from "node:net";
|
|
4
4
|
|
|
5
5
|
export class UrlError extends Error {
|
|
6
|
-
constructor(message: string) {
|
|
7
|
-
super(message);
|
|
6
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
7
|
+
super(message, options);
|
|
8
8
|
this.name = "UrlError";
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -51,12 +51,42 @@ export async function validatePublicUrl(url: string): Promise<void> {
|
|
|
51
51
|
let addresses: LookupAddress[];
|
|
52
52
|
try {
|
|
53
53
|
addresses = await lookup(hostname, { all: true });
|
|
54
|
-
} catch {
|
|
55
|
-
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new UrlError("DNS lookup failed", { cause: error });
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
validateLookupAddresses(addresses);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validates the IP addresses returned by `node:dns.lookup()`.
|
|
62
|
+
*
|
|
63
|
+
* Cloudflare Workers' `node:dns` implementation currently maps every record
|
|
64
|
+
* in a DNS-over-HTTPS `Answer` array—including CNAME records—to a
|
|
65
|
+
* `LookupAddress`, even though Node.js specifies that the `address` field must
|
|
66
|
+
* contain an IPv4 or IPv6 literal. See:
|
|
67
|
+
* https://github.com/cloudflare/workerd/issues/6886
|
|
68
|
+
*
|
|
69
|
+
* Work around that bug by ignoring non-IP entries only when the lookup also
|
|
70
|
+
* returns at least one actual IP address. This remains fail-closed: a result
|
|
71
|
+
* containing no IP addresses is rejected, and every returned IP address is
|
|
72
|
+
* still validated and must be public. This workaround can be revisited once
|
|
73
|
+
* the Workerd issue is fixed in supported Cloudflare Workers runtimes.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
export function validateLookupAddresses(
|
|
78
|
+
addresses: readonly LookupAddress[],
|
|
79
|
+
): void {
|
|
80
|
+
let ipAddressCount = 0;
|
|
81
|
+
for (const { address } of addresses) {
|
|
82
|
+
const family = isIP(address);
|
|
83
|
+
if (family === 0) continue;
|
|
84
|
+
ipAddressCount++;
|
|
58
85
|
validatePublicIpAddress(address, family);
|
|
59
86
|
}
|
|
87
|
+
if (ipAddressCount === 0) {
|
|
88
|
+
throw new UrlError("DNS lookup did not return any IP address");
|
|
89
|
+
}
|
|
60
90
|
}
|
|
61
91
|
|
|
62
92
|
function validatePublicIpAddress(address: string, family: number): void {
|