patchwork-os 1.2.0-beta.3.canary.875 → 1.2.0-beta.3.canary.877

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.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Lexical private-range checks for hostnames and IP literals — pure string
3
+ * work, no DNS and no network.
4
+ *
5
+ * Split out of `ssrfGuard.ts` (2026-10-02) so code that only needs the range
6
+ * decision does not inherit ssrfGuard's I/O dependencies. The trigger: the
7
+ * dashboard's push-endpoint validator imports `isPrivateHost`, and once
8
+ * ssrfGuard imported `undici` (to pin connections) the dashboard package —
9
+ * which does not depend on undici — failed to typecheck. `ssrfGuard.ts`
10
+ * re-exports everything here, so every existing importer is unchanged.
11
+ */
12
+ /**
13
+ * Convert the hex-compressed form that `new URL()` may return for an
14
+ * IPv4-mapped/translated IPv6 address back to dotted-decimal so the existing
15
+ * `isPrivateHost` checks apply.
16
+ *
17
+ * Handles:
18
+ * "xxxx:yyyy" — two 16-bit groups (up to 4 hex digits each, no leading zeros)
19
+ * "xxxxxxxx" — a single 32-bit group
20
+ *
21
+ * Returns null when the input is not recognisable as 32-bit hex-IPv4.
22
+ */
23
+ function hexIpv4ToDotted(s) {
24
+ const colon = s.indexOf(":");
25
+ if (colon !== -1) {
26
+ const hi = s.slice(0, colon);
27
+ const lo = s.slice(colon + 1);
28
+ if (!/^[0-9a-f]{1,4}$/i.test(hi) || !/^[0-9a-f]{1,4}$/i.test(lo))
29
+ return null;
30
+ const hiN = parseInt(hi, 16);
31
+ const loN = parseInt(lo, 16);
32
+ return `${(hiN >>> 8) & 0xff}.${hiN & 0xff}.${(loN >>> 8) & 0xff}.${loN & 0xff}`;
33
+ }
34
+ if (!/^[0-9a-f]{1,8}$/i.test(s))
35
+ return null;
36
+ const n = parseInt(s, 16);
37
+ return `${(n >>> 24) & 0xff}.${(n >>> 16) & 0xff}.${(n >>> 8) & 0xff}.${n & 0xff}`;
38
+ }
39
+ /**
40
+ * inet_aton-style loose IPv4 parse: 1-4 parts, each decimal, octal (leading
41
+ * 0) or hex (0x). Returns canonical dotted-quad or null when the string is
42
+ * not an all-numeric IPv4 form. `new URL()` already canonicalises these, but
43
+ * callers that hand `isPrivateHost` a raw string (a DNS answer, a Location
44
+ * header hostname) must not be bypassable by `0x7f000001`, `2130706433`,
45
+ * `0177.0.0.1` or `127.1`.
46
+ */
47
+ function looseIpv4ToDotted(host) {
48
+ if (!/^[0-9a-fx.]+$/i.test(host) ||
49
+ host.startsWith(".") ||
50
+ host.endsWith("."))
51
+ return null;
52
+ const parts = host.split(".");
53
+ if (parts.length < 1 || parts.length > 4)
54
+ return null;
55
+ const nums = [];
56
+ for (const part of parts) {
57
+ let n;
58
+ if (/^0x[0-9a-f]+$/i.test(part))
59
+ n = parseInt(part.slice(2), 16);
60
+ else if (/^0[0-7]+$/.test(part))
61
+ n = parseInt(part, 8);
62
+ else if (/^\d+$/.test(part))
63
+ n = parseInt(part, 10);
64
+ else
65
+ return null;
66
+ if (!Number.isFinite(n))
67
+ return null;
68
+ nums.push(n);
69
+ }
70
+ // Last part fills the remaining bytes (inet_aton semantics).
71
+ const last = nums[nums.length - 1];
72
+ const remainingBytes = 4 - (nums.length - 1);
73
+ if (last >= 2 ** (8 * remainingBytes))
74
+ return null;
75
+ for (let i = 0; i < nums.length - 1; i++) {
76
+ if (nums[i] > 255)
77
+ return null;
78
+ }
79
+ let value = 0;
80
+ for (let i = 0; i < nums.length - 1; i++) {
81
+ value = value * 256 + nums[i];
82
+ }
83
+ value = value * 2 ** (8 * remainingBytes) + last;
84
+ return `${(value >>> 24) & 0xff}.${(value >>> 16) & 0xff}.${(value >>> 8) & 0xff}.${value & 0xff}`;
85
+ }
86
+ /**
87
+ * Block requests to private/loopback addresses. Lexical-only check.
88
+ *
89
+ * Mirrors the predicate previously inlined in `tools/httpClient.ts`. Updates
90
+ * here MUST stay in sync with the test fixtures in
91
+ * `src/tools/__tests__/httpClient.test.ts`.
92
+ */
93
+ export function isPrivateHost(hostname) {
94
+ const host = hostname.startsWith("[") && hostname.endsWith("]")
95
+ ? hostname.slice(1, -1).toLowerCase()
96
+ : hostname.toLowerCase();
97
+ if (host === "localhost" || host.endsWith(".localhost"))
98
+ return true;
99
+ if (host === "0.0.0.0")
100
+ return true;
101
+ // Reject non-decimal IPv4 notations (hex/octal) that bypass the dotted-quad
102
+ // regex below. Node's URL parser may normalize them on some platforms.
103
+ if (/^0x[0-9a-f]+$/i.test(host) || /^0[0-7]{7,}$/.test(host))
104
+ return true;
105
+ // Unusual IPv4 notations (decimal integer, short-form "127.1", zero-padded
106
+ // octal, per-part hex): canonicalise and re-check the dotted form.
107
+ const canonicalIpv4 = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
108
+ if (!canonicalIpv4) {
109
+ const dotted = looseIpv4ToDotted(host);
110
+ if (dotted !== null)
111
+ return isPrivateHost(dotted);
112
+ }
113
+ // IPv4 range checks
114
+ const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
115
+ if (ipv4) {
116
+ const a = Number(ipv4[1]);
117
+ const b = Number(ipv4[2]);
118
+ if (a === 127)
119
+ return true; // 127.0.0.0/8 loopback
120
+ if (a === 10)
121
+ return true; // 10.0.0.0/8 RFC 1918 private
122
+ if (a === 172 && b >= 16 && b <= 31)
123
+ return true; // 172.16.0.0/12 RFC 1918 private
124
+ if (a === 192 && b === 168)
125
+ return true; // 192.168.0.0/16 RFC 1918 private
126
+ if (a === 169 && b === 254)
127
+ return true; // 169.254.0.0/16 link-local / AWS metadata
128
+ if (a === 100 && b >= 64 && b <= 127)
129
+ return true; // 100.64.0.0/10 CGNAT (RFC 6598)
130
+ if (a === 0)
131
+ return true; // 0.0.0.0/8
132
+ }
133
+ // IPv6 checks
134
+ if (host === "::1")
135
+ return true; // loopback
136
+ if (host.startsWith("fe80:"))
137
+ return true; // link-local
138
+ if (host.startsWith("fc") || host.startsWith("fd"))
139
+ return true; // ULA (RFC 4193)
140
+ if (host.startsWith("2002:"))
141
+ return true; // 6to4 (RFC 3056) — embeds IPv4 in bits 16-47;
142
+ // a 6to4 address for a private IPv4 (e.g. 2002:c0a8:0101:: → 192.168.1.1) bypasses
143
+ // the IPv4 checks above unless we block the entire /16 here.
144
+ // Check longer prefix first — ::ffff:0: (IPv4-translated) before ::ffff: (IPv4-mapped)
145
+ // Also handle hex-compressed form that new URL() may return (e.g. "7f00:1" = 127.0.0.1).
146
+ if (host.startsWith("::ffff:0:")) {
147
+ const rest = host.slice(9);
148
+ const dotted = hexIpv4ToDotted(rest);
149
+ return isPrivateHost(dotted ?? rest);
150
+ }
151
+ if (host.startsWith("::ffff:")) {
152
+ const rest = host.slice(7);
153
+ const dotted = hexIpv4ToDotted(rest);
154
+ return isPrivateHost(dotted ?? rest);
155
+ }
156
+ return false;
157
+ }
158
+ /**
159
+ * Loopback-only check (127.0.0.0/8, ::1, localhost). Lexical-only.
160
+ *
161
+ * Used by `isPrivateNonLoopbackHost` and by automation webhook fan-out, which
162
+ * intentionally ALLOWS loopback (sidecars) but blocks every other private
163
+ * range (RFC 1918, link-local, ULA, IMDS, 6to4-wrapped private, etc.).
164
+ */
165
+ export function isLoopbackHost(hostname) {
166
+ const host = hostname.startsWith("[") && hostname.endsWith("]")
167
+ ? hostname.slice(1, -1).toLowerCase()
168
+ : hostname.toLowerCase();
169
+ if (host === "localhost" || host.endsWith(".localhost"))
170
+ return true;
171
+ if (host === "::1")
172
+ return true;
173
+ const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
174
+ if (ipv4 && Number(ipv4[1]) === 127)
175
+ return true;
176
+ // IPv6-mapped/translated loopback: ::ffff:127.0.0.1 / ::ffff:0:127.0.0.1
177
+ // Also handle hex-compressed form from new URL() (e.g. "7f00:1" = 127.0.0.1).
178
+ if (host.startsWith("::ffff:0:")) {
179
+ const rest = host.slice(9);
180
+ const dotted = hexIpv4ToDotted(rest);
181
+ return isLoopbackHost(dotted ?? rest);
182
+ }
183
+ if (host.startsWith("::ffff:")) {
184
+ const rest = host.slice(7);
185
+ const dotted = hexIpv4ToDotted(rest);
186
+ return isLoopbackHost(dotted ?? rest);
187
+ }
188
+ return false;
189
+ }
190
+ /**
191
+ * Private host MINUS loopback. Lexical-only.
192
+ *
193
+ * Use when a sink intentionally allows loopback (e.g. webhook fan-out to local
194
+ * sidecars) but must still block RFC 1918, link-local, IMDS, ULA, 6to4-wrapped
195
+ * private, and IPv4-mapped/translated private addresses.
196
+ */
197
+ export function isPrivateNonLoopbackHost(hostname) {
198
+ if (isLoopbackHost(hostname))
199
+ return false;
200
+ return isPrivateHost(hostname);
201
+ }
202
+ //# sourceMappingURL=privateHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privateHost.js","sourceRoot":"","sources":["../src/privateHost.ts"],"names":[],"mappings":"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"}
@@ -10,7 +10,7 @@
10
10
  * the recipe explicitly opts in with `allowPrivate: true`.
11
11
  */
12
12
  import dns from "node:dns/promises";
13
- import { Agent, fetch as undiciFetch } from "undici";
13
+ import { fetch as undiciFetch } from "undici";
14
14
  import { assertWriteAllowed, FLAG_BLOCK_RECIPE_ALLOW_PRIVATE, isEnabled, } from "../../featureFlags.js";
15
15
  // Canonical SSRF guard — single source of truth. `isPrivateHost` is
16
16
  // re-exported below for the lexical unit tests; execution goes through
@@ -19,24 +19,19 @@ import { assertWriteAllowed, FLAG_BLOCK_RECIPE_ALLOW_PRIVATE, isEnabled, } from
19
19
  import { isPrivateHost, OutboundHttpError, safeFetch, } from "../../ssrfGuard.js";
20
20
  import { CommonSchemas, registerTool } from "../toolRegistry.js";
21
21
  import { UncertainOutcomeError } from "../uncertainOutcome.js";
22
- // Custom dispatcher pinning DNS resolution to IPv4. Node's Happy-Eyeballs
23
- // implementation (autoSelectFamily) is documented to flip families after
24
- // 250 ms, but on macOS networks that lack a usable IPv6 path
25
- // (most home/office LANs, despite the host having public AAAA records)
26
- // the IPv6 attempt stalls past Node's request timeout and surfaces as
27
- // ETIMEDOUT — even though IPv4 would have succeeded instantly. Probing
28
- // repro'd this against ntfy.sh on 2026-05-12.
22
+ // IPv4 only, for both DNS (`resolveDns` below) and the socket (`connect`
23
+ // passed to safeFetch). Node's Happy-Eyeballs implementation
24
+ // (autoSelectFamily) is documented to flip families after 250 ms, but on
25
+ // macOS networks that lack a usable IPv6 path (most home/office LANs,
26
+ // despite the host having public AAAA records) the IPv6 attempt stalls past
27
+ // Node's request timeout and surfaces as ETIMEDOUT — even though IPv4 would
28
+ // have succeeded instantly. Probing repro'd this against ntfy.sh on
29
+ // 2026-05-12. IPv6-only networks are vanishingly rare in 2026; if one comes
30
+ // up we can add a step-level `family: 6` override.
29
31
  //
30
- // IPv6-only networks are vanishingly rare in 2026; if one comes up we
31
- // can add a step-level `family: 6` override.
32
- const httpAgent = new Agent({
33
- // biome-ignore lint/suspicious/noExplicitAny: undici's TcpNetConnectOpts type insists on `port` but it isn't required at the Agent-default-connect level
34
- connect: { family: 4 },
35
- // Keep idle sockets short to avoid stale connections to the same host
36
- // hanging across recipe fires.
37
- keepAliveTimeout: 5_000,
38
- keepAliveMaxTimeout: 10_000,
39
- });
32
+ // There is no shared agent any more: safeFetch builds a dispatcher per hop
33
+ // because that dispatcher IS the DNS pin (2026-10-02 — pinning by rewriting
34
+ // the URL to the IP broke every public HTTPS request).
40
35
  /**
41
36
  * The "sent" boundary. A per-request view over the shared agent whose ONLY
42
37
  * job is to record whether undici ever handed this request to a socket.
@@ -59,14 +54,18 @@ const httpAgent = new Agent({
59
54
  */
60
55
  function trackedDispatcher() {
61
56
  let sent = false;
62
- const dispatcher = httpAgent.compose((dispatch) => (opts, handler) => dispatch(opts, {
57
+ // The same interceptor applied to ANY dispatcher: safeFetch now builds the
58
+ // per-hop dispatcher itself (that is where the DNS pin lives), so the
59
+ // sent-boundary has to wrap the dispatcher it was given rather than a
60
+ // shared agent the pin cannot reach.
61
+ const wrap = (d) => d.compose((dispatch) => (opts, handler) => dispatch(opts, {
63
62
  ...handler,
64
63
  onConnect(abort) {
65
64
  sent = true;
66
65
  handler.onConnect?.(abort);
67
66
  },
68
67
  }));
69
- return { dispatcher, sent: () => sent };
68
+ return { wrap, sent: () => sent };
70
69
  }
71
70
  registerTool({
72
71
  id: "http.post",
@@ -185,9 +184,13 @@ registerTool({
185
184
  body,
186
185
  headers,
187
186
  signal: ctrl.signal,
188
- dispatcher: tracked.dispatcher,
189
187
  }, {
190
188
  allowPrivate,
189
+ // IPv4 for the same reason as `httpAgent` above; safeFetch builds
190
+ // the per-hop dispatcher (it holds the DNS pin), so the family
191
+ // preference and the sent-boundary tracking ride on it.
192
+ connect: { family: 4 },
193
+ wrapDispatcher: tracked.wrap,
191
194
  resolveDns: async (hostname) => (await dns.lookup(hostname, { family: 4 })).address,
192
195
  // undici's Response type lags the DOM lib (no `bytes()`); same
193
196
  // object at runtime.
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/recipes/tools/http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/recipes/tools/http.ts"],"names":[],"mappings":"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"}
@@ -26,13 +26,24 @@
26
26
  * both walked straight past it):
27
27
  * - `validateOutboundUrl(url, opts)` — parse, protocol, userinfo strip,
28
28
  * lexical check, DNS pre-resolution re-check, returns the address to pin.
29
- * - `safeFetch(url, init, opts)` — validates, pins the connection to the
30
- * resolved address (hostname rewritten to the IP, original name carried
31
- * in the Host header so SNI / virtual hosting keep working), follows up
32
- * to N redirects manually re-validating EVERY hop, downgrades method/body
33
- * per RFC 7231 on 301/302/303, and drops credential headers on a
34
- * cross-origin hop.
29
+ * - `safeFetch(url, init, opts)` — validates, pins the CONNECTION to the
30
+ * resolved address through an undici dispatcher whose `connect.lookup`
31
+ * returns that address, follows up to N redirects manually re-validating
32
+ * EVERY hop, downgrades method/body per RFC 7231 on 301/302/303, and
33
+ * drops credential headers on a cross-origin hop.
34
+ *
35
+ * The pin lives in the connection, never in the URL. #1572 first pinned by
36
+ * rewriting the URL's hostname to the IP and carrying the name in a `Host`
37
+ * header; TLS takes SNI and the certificate check from the URL, so every
38
+ * public HTTPS request failed ERR_TLS_CERT_ALTNAME_INVALID, and `fetch`
39
+ * silently drops a caller-set `Host`, so plain HTTP reached virtual hosts
40
+ * with the wrong one. Found 2026-10-02 against the installed build; nothing
41
+ * in the suite could see it because every test used a mocked fetch or a
42
+ * loopback HTTP server.
35
43
  */
44
+ import { type Dispatcher } from "undici";
45
+ import { isLoopbackHost, isPrivateHost, isPrivateNonLoopbackHost } from "./privateHost.js";
46
+ export { isLoopbackHost, isPrivateHost, isPrivateNonLoopbackHost };
36
47
  export interface UrlValidationResult {
37
48
  ok: boolean;
38
49
  /** Parsed URL when ok === true. */
@@ -44,30 +55,6 @@ export interface UrlValidationResult {
44
55
  /** Human-readable detail for logs. */
45
56
  detail?: string;
46
57
  }
47
- /**
48
- * Block requests to private/loopback addresses. Lexical-only check.
49
- *
50
- * Mirrors the predicate previously inlined in `tools/httpClient.ts`. Updates
51
- * here MUST stay in sync with the test fixtures in
52
- * `src/tools/__tests__/httpClient.test.ts`.
53
- */
54
- export declare function isPrivateHost(hostname: string): boolean;
55
- /**
56
- * Loopback-only check (127.0.0.0/8, ::1, localhost). Lexical-only.
57
- *
58
- * Used by `isPrivateNonLoopbackHost` and by automation webhook fan-out, which
59
- * intentionally ALLOWS loopback (sidecars) but blocks every other private
60
- * range (RFC 1918, link-local, ULA, IMDS, 6to4-wrapped private, etc.).
61
- */
62
- export declare function isLoopbackHost(hostname: string): boolean;
63
- /**
64
- * Private host MINUS loopback. Lexical-only.
65
- *
66
- * Use when a sink intentionally allows loopback (e.g. webhook fan-out to local
67
- * sidecars) but must still block RFC 1918, link-local, IMDS, ULA, 6to4-wrapped
68
- * private, and IPv4-mapped/translated private addresses.
69
- */
70
- export declare function isPrivateNonLoopbackHost(hostname: string): boolean;
71
58
  /**
72
59
  * Async URL safety gate used by `/recipes/install` and `commands/recipeInstall.ts`.
73
60
  *
@@ -124,8 +111,24 @@ export declare class OutboundHttpError extends Error {
124
111
  /** Minimal fetch shape so undici's fetch and globalThis.fetch both fit. */
125
112
  export type FetchLike = (url: string, init: Record<string, unknown>) => Promise<Response>;
126
113
  export interface SafeFetchOptions extends OutboundUrlOptions {
127
- /** Defaults to `globalThis.fetch`. */
114
+ /**
115
+ * Defaults to `globalThis.fetch`. Node's built-in fetch accepts the pinning
116
+ * `Agent` from the `undici` package as its `dispatcher` — verified against a
117
+ * public HTTPS endpoint on Node 24 (2026-10-02).
118
+ */
128
119
  fetchImpl?: FetchLike;
120
+ /**
121
+ * Extra socket options for every hop's connection (e.g. `{ family: 4 }`).
122
+ * `lookup` is always overridden when a hop is pinned.
123
+ */
124
+ connect?: Record<string, unknown>;
125
+ /**
126
+ * Wrap the per-hop dispatcher safeFetch builds — the seam `http.post` uses
127
+ * to observe `onConnect` (its "was anything sent" boundary). A caller's
128
+ * `init.dispatcher` is IGNORED: honouring it would let a caller route
129
+ * around the pin, which is the whole guard.
130
+ */
131
+ wrapDispatcher?: (d: Dispatcher) => Dispatcher;
129
132
  /** Follow 3xx redirects (default true). */
130
133
  followRedirects?: boolean;
131
134
  /** Redirect hop cap (default 10). */
@@ -143,9 +146,11 @@ export interface SafeFetchResult {
143
146
  * refusal (initial URL or any redirect hop); transport errors propagate from
144
147
  * `fetchImpl` untouched so callers keep their own timeout/abort messages.
145
148
  *
146
- * `init.headers` must be a plain object; keys are lowercased. `host` is set
147
- * by this function AFTER caller headers so a caller cannot un-pin a hop.
148
- * Every other `init` field (signal, dispatcher, …) is passed through.
149
+ * `init.headers` must be a plain object; keys are lowercased. A caller's
150
+ * `host` header and `dispatcher` are DROPPED: Host is derived from the URL,
151
+ * and the dispatcher is the pin — accepting either from the caller is a way
152
+ * to steer the request somewhere the validation never looked. Every other
153
+ * `init` field (signal, …) is passed through.
149
154
  */
150
155
  export declare function safeFetch(input: string | URL, init?: Record<string, unknown> & {
151
156
  method?: string;