@dnsid-ai/transport 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +8 -20
- package/dist/index.d.cts +7 -9
- package/dist/index.d.ts +7 -9
- package/dist/index.js +9 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,4 +41,4 @@ Most Node applications should use this through `@dnsid-ai/sdk/node` or profile d
|
|
|
41
41
|
|
|
42
42
|
Node's TXT lookup API exposes neither remaining TTLs nor DNSSEC validation state. Both system and configured-server resolvers therefore return TTL `0` and `DNSSECState.UNKNOWN`: fresh verification works in `auto` mode, but SDK identity caching is disabled and `validated`/`required` modes reject the unknown DNSSEC state. To enable caching, inject a `DNSResolver` that supplies real remaining TTLs; the published zone TTL is not a safe substitute for a recursive resolver's remaining TTL.
|
|
43
43
|
|
|
44
|
-
`createSsrfSafeFetch()` enforces unsafe-address rejection in the lookup used by the outgoing socket and returns redirects without following them automatically. Trusted test/private deployments may pass
|
|
44
|
+
`createSsrfSafeFetch()` enforces unsafe-address rejection in the lookup used by the outgoing socket and returns redirects without following them automatically. Trusted test/private deployments may pass `privateAddressHosts`: exact hostnames or leading-dot suffixes such as `.test` (label-bounded, case-insensitive); only RFC 1918/ULA private and loopback results are then accepted for matching hosts, while link-local, mixed public/private, and IP-literal URLs remain blocked. Nothing is allowed by default, not even `.test`; a local `dnsid` stack needs `privateAddressHosts: ['.test']` (or `DNSID_PRIVATE_HOSTS=.test` through `loadEnvironment`). Profile packages that accept a custom `fetch` cannot force equivalent behavior on arbitrary implementations, so custom fetch injection remains trusted infrastructure.
|
package/dist/index.cjs
CHANGED
|
@@ -61,13 +61,13 @@ function createDnsidFetch(config) {
|
|
|
61
61
|
return createFetchWithDispatcher(new import_undici.Agent({ connect }));
|
|
62
62
|
}
|
|
63
63
|
function createSsrfSafeFetch(config = {}, options = {}) {
|
|
64
|
-
const
|
|
65
|
-
const connect = { lookup: createSsrfSafeLookup(config.dnsServer,
|
|
64
|
+
const privateAddressHosts = (options.privateAddressHosts ?? []).map(import_protocol.normalizePrivateAddressHost);
|
|
65
|
+
const connect = { lookup: createSsrfSafeLookup(config.dnsServer, privateAddressHosts) };
|
|
66
66
|
if (config.caBundlePath) connect.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
|
|
67
67
|
const fetchWithSafeLookup = createFetchWithDispatcher(new import_undici.Agent({ connect }));
|
|
68
68
|
return async (input, init) => {
|
|
69
69
|
const url = new URL(input instanceof Request ? input.url : input);
|
|
70
|
-
if (isUnsafeIp(url.hostname)
|
|
70
|
+
if (isUnsafeIp(url.hostname)) {
|
|
71
71
|
throw new import_protocol.VerificationError(`unsafe target IP address: ${url.hostname}`, {
|
|
72
72
|
code: import_protocol.VerificationCode.TLSError
|
|
73
73
|
});
|
|
@@ -152,7 +152,7 @@ function createLookup(dnsServer) {
|
|
|
152
152
|
})().catch((err) => cb(err, "", 0));
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
-
function createSsrfSafeLookup(dnsServer,
|
|
155
|
+
function createSsrfSafeLookup(dnsServer, privateAddressHosts = []) {
|
|
156
156
|
let resolverPromise = null;
|
|
157
157
|
async function customResolver() {
|
|
158
158
|
if (!dnsServer) throw new Error("no custom DNS server configured");
|
|
@@ -172,7 +172,7 @@ function createSsrfSafeLookup(dnsServer, allowedUnsafeHosts = /* @__PURE__ */ ne
|
|
|
172
172
|
void (async () => {
|
|
173
173
|
const addresses = await resolveAddresses(hostname, family, dnsServer ? await customResolver() : void 0);
|
|
174
174
|
const unsafe = addresses.find(({ address }) => isUnsafeIp(address));
|
|
175
|
-
if (unsafe && !(
|
|
175
|
+
if (unsafe && !(privateAddressHostMatches(hostname, privateAddressHosts) && addresses.every(({ address }) => isPrivateOrLoopbackIp(address)))) {
|
|
176
176
|
throw new import_protocol.VerificationError(`unsafe resolved IP address for ${hostname}: ${unsafe.address}`, {
|
|
177
177
|
code: import_protocol.VerificationCode.TLSError
|
|
178
178
|
});
|
|
@@ -185,21 +185,9 @@ function createSsrfSafeLookup(dnsServer, allowedUnsafeHosts = /* @__PURE__ */ ne
|
|
|
185
185
|
})().catch((err) => cb(err, "", 0));
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
|
-
function
|
|
188
|
+
function privateAddressHostMatches(hostname, privateAddressHosts) {
|
|
189
189
|
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
function normalizeAllowedUnsafeHost(host) {
|
|
193
|
-
let parsed;
|
|
194
|
-
try {
|
|
195
|
-
parsed = new URL(`https://${host}`);
|
|
196
|
-
} catch {
|
|
197
|
-
throw new TypeError(`invalid allowed unsafe hostname: ${host}`);
|
|
198
|
-
}
|
|
199
|
-
if (!host || parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
200
|
-
throw new TypeError(`invalid allowed unsafe hostname: ${host}`);
|
|
201
|
-
}
|
|
202
|
-
return parsed.hostname;
|
|
190
|
+
return privateAddressHosts.some((entry) => entry.startsWith(".") ? host === entry.slice(1) || host.endsWith(entry) : host === entry);
|
|
203
191
|
}
|
|
204
192
|
async function resolveAddresses(hostname, family, resolver) {
|
|
205
193
|
const ipFamily = net.isIP(hostname);
|
|
@@ -381,7 +369,7 @@ function fetchWithRedirects(url, opts, depth) {
|
|
|
381
369
|
headers: {
|
|
382
370
|
"User-Agent": DNSID_USER_AGENT
|
|
383
371
|
},
|
|
384
|
-
lookup: createSsrfSafeLookup(opts.dnsServer,
|
|
372
|
+
lookup: createSsrfSafeLookup(opts.dnsServer, (opts.privateAddressHosts ?? []).map(import_protocol.normalizePrivateAddressHost))
|
|
385
373
|
};
|
|
386
374
|
if (opts.caBundlePath) requestOptions.ca = [...tls.rootCertificates, fs.readFileSync(opts.caBundlePath, "utf8")];
|
|
387
375
|
const req = https.get(url, requestOptions, (res) => {
|
package/dist/index.d.cts
CHANGED
|
@@ -27,21 +27,19 @@ interface HTTPSFetchOptions {
|
|
|
27
27
|
dnsServer?: string;
|
|
28
28
|
/** Maximum accepted response body size in bytes. Defaults to 1 MiB. */
|
|
29
29
|
maxResponseBytes?: number;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
* private deployments only; public defaults use none.
|
|
33
|
-
*/
|
|
34
|
-
allowedUnsafeHosts?: readonly string[];
|
|
30
|
+
/** Same as `TransportConfig.privateAddressHosts`: hostnames or `.suffix` entries that may resolve to loopback/private addresses. */
|
|
31
|
+
privateAddressHosts?: readonly string[];
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
/** Explicit exceptions for trusted test/private deployments using {@link createSsrfSafeFetch}. */
|
|
38
35
|
interface SsrfSafeFetchOptions {
|
|
39
36
|
/**
|
|
40
|
-
* Hostnames whose resolved RFC 1918/ULA private or
|
|
41
|
-
* Link-local and other unsafe ranges remain blocked
|
|
42
|
-
*
|
|
37
|
+
* Hostnames, or leading-dot suffixes such as `.test`, whose resolved RFC 1918/ULA private or
|
|
38
|
+
* loopback addresses may be contacted. Link-local and other unsafe ranges remain blocked, IP-literal
|
|
39
|
+
* URLs are never exempted, and nothing is allowed by default. Same semantics as
|
|
40
|
+
* `TransportConfig.privateAddressHosts`.
|
|
43
41
|
*/
|
|
44
|
-
|
|
42
|
+
privateAddressHosts?: readonly string[];
|
|
45
43
|
}
|
|
46
44
|
/**
|
|
47
45
|
* Creates a fetch function honoring the transport configuration.
|
package/dist/index.d.ts
CHANGED
|
@@ -27,21 +27,19 @@ interface HTTPSFetchOptions {
|
|
|
27
27
|
dnsServer?: string;
|
|
28
28
|
/** Maximum accepted response body size in bytes. Defaults to 1 MiB. */
|
|
29
29
|
maxResponseBytes?: number;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
* private deployments only; public defaults use none.
|
|
33
|
-
*/
|
|
34
|
-
allowedUnsafeHosts?: readonly string[];
|
|
30
|
+
/** Same as `TransportConfig.privateAddressHosts`: hostnames or `.suffix` entries that may resolve to loopback/private addresses. */
|
|
31
|
+
privateAddressHosts?: readonly string[];
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
/** Explicit exceptions for trusted test/private deployments using {@link createSsrfSafeFetch}. */
|
|
38
35
|
interface SsrfSafeFetchOptions {
|
|
39
36
|
/**
|
|
40
|
-
* Hostnames whose resolved RFC 1918/ULA private or
|
|
41
|
-
* Link-local and other unsafe ranges remain blocked
|
|
42
|
-
*
|
|
37
|
+
* Hostnames, or leading-dot suffixes such as `.test`, whose resolved RFC 1918/ULA private or
|
|
38
|
+
* loopback addresses may be contacted. Link-local and other unsafe ranges remain blocked, IP-literal
|
|
39
|
+
* URLs are never exempted, and nothing is allowed by default. Same semantics as
|
|
40
|
+
* `TransportConfig.privateAddressHosts`.
|
|
43
41
|
*/
|
|
44
|
-
|
|
42
|
+
privateAddressHosts?: readonly string[];
|
|
45
43
|
}
|
|
46
44
|
/**
|
|
47
45
|
* Creates a fetch function honoring the transport configuration.
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as https from "https";
|
|
|
5
5
|
import * as net from "net";
|
|
6
6
|
import * as tls from "tls";
|
|
7
7
|
import { Agent as Agent2, fetch as undiciFetch } from "undici";
|
|
8
|
-
import { DNSSECState, VerificationCode, VerificationError } from "@dnsid-ai/protocol";
|
|
8
|
+
import { DNSSECState, VerificationCode, VerificationError, normalizePrivateAddressHost } from "@dnsid-ai/protocol";
|
|
9
9
|
var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
10
10
|
var DEFAULT_MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
|
|
11
11
|
var DNSID_USER_AGENT = "dnsid-ts";
|
|
@@ -19,13 +19,13 @@ function createDnsidFetch(config) {
|
|
|
19
19
|
return createFetchWithDispatcher(new Agent2({ connect }));
|
|
20
20
|
}
|
|
21
21
|
function createSsrfSafeFetch(config = {}, options = {}) {
|
|
22
|
-
const
|
|
23
|
-
const connect = { lookup: createSsrfSafeLookup(config.dnsServer,
|
|
22
|
+
const privateAddressHosts = (options.privateAddressHosts ?? []).map(normalizePrivateAddressHost);
|
|
23
|
+
const connect = { lookup: createSsrfSafeLookup(config.dnsServer, privateAddressHosts) };
|
|
24
24
|
if (config.caBundlePath) connect.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
|
|
25
25
|
const fetchWithSafeLookup = createFetchWithDispatcher(new Agent2({ connect }));
|
|
26
26
|
return async (input, init) => {
|
|
27
27
|
const url = new URL(input instanceof Request ? input.url : input);
|
|
28
|
-
if (isUnsafeIp(url.hostname)
|
|
28
|
+
if (isUnsafeIp(url.hostname)) {
|
|
29
29
|
throw new VerificationError(`unsafe target IP address: ${url.hostname}`, {
|
|
30
30
|
code: VerificationCode.TLSError
|
|
31
31
|
});
|
|
@@ -110,7 +110,7 @@ function createLookup(dnsServer) {
|
|
|
110
110
|
})().catch((err) => cb(err, "", 0));
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
-
function createSsrfSafeLookup(dnsServer,
|
|
113
|
+
function createSsrfSafeLookup(dnsServer, privateAddressHosts = []) {
|
|
114
114
|
let resolverPromise = null;
|
|
115
115
|
async function customResolver() {
|
|
116
116
|
if (!dnsServer) throw new Error("no custom DNS server configured");
|
|
@@ -130,7 +130,7 @@ function createSsrfSafeLookup(dnsServer, allowedUnsafeHosts = /* @__PURE__ */ ne
|
|
|
130
130
|
void (async () => {
|
|
131
131
|
const addresses = await resolveAddresses(hostname, family, dnsServer ? await customResolver() : void 0);
|
|
132
132
|
const unsafe = addresses.find(({ address }) => isUnsafeIp(address));
|
|
133
|
-
if (unsafe && !(
|
|
133
|
+
if (unsafe && !(privateAddressHostMatches(hostname, privateAddressHosts) && addresses.every(({ address }) => isPrivateOrLoopbackIp(address)))) {
|
|
134
134
|
throw new VerificationError(`unsafe resolved IP address for ${hostname}: ${unsafe.address}`, {
|
|
135
135
|
code: VerificationCode.TLSError
|
|
136
136
|
});
|
|
@@ -143,21 +143,9 @@ function createSsrfSafeLookup(dnsServer, allowedUnsafeHosts = /* @__PURE__ */ ne
|
|
|
143
143
|
})().catch((err) => cb(err, "", 0));
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
|
-
function
|
|
146
|
+
function privateAddressHostMatches(hostname, privateAddressHosts) {
|
|
147
147
|
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
function normalizeAllowedUnsafeHost(host) {
|
|
151
|
-
let parsed;
|
|
152
|
-
try {
|
|
153
|
-
parsed = new URL(`https://${host}`);
|
|
154
|
-
} catch {
|
|
155
|
-
throw new TypeError(`invalid allowed unsafe hostname: ${host}`);
|
|
156
|
-
}
|
|
157
|
-
if (!host || parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
158
|
-
throw new TypeError(`invalid allowed unsafe hostname: ${host}`);
|
|
159
|
-
}
|
|
160
|
-
return parsed.hostname;
|
|
148
|
+
return privateAddressHosts.some((entry) => entry.startsWith(".") ? host === entry.slice(1) || host.endsWith(entry) : host === entry);
|
|
161
149
|
}
|
|
162
150
|
async function resolveAddresses(hostname, family, resolver) {
|
|
163
151
|
const ipFamily = net.isIP(hostname);
|
|
@@ -339,7 +327,7 @@ function fetchWithRedirects(url, opts, depth) {
|
|
|
339
327
|
headers: {
|
|
340
328
|
"User-Agent": DNSID_USER_AGENT
|
|
341
329
|
},
|
|
342
|
-
lookup: createSsrfSafeLookup(opts.dnsServer,
|
|
330
|
+
lookup: createSsrfSafeLookup(opts.dnsServer, (opts.privateAddressHosts ?? []).map(normalizePrivateAddressHost))
|
|
343
331
|
};
|
|
344
332
|
if (opts.caBundlePath) requestOptions.ca = [...tls.rootCertificates, fs.readFileSync(opts.caBundlePath, "utf8")];
|
|
345
333
|
const req = https.get(url, requestOptions, (res) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dnsid-ai/transport",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Node.js DNS and HTTPS transport implementations for DNSid.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"typecheck": "tsc --noEmit"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@dnsid-ai/protocol": "^0.
|
|
41
|
+
"@dnsid-ai/protocol": "^0.23.0",
|
|
42
42
|
"undici": "^8.10.2"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|