SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%

ssrf: fall back to public resolvers when the system resolver fails; homepage reveal SSR-visible; chart formatter keys

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 16 days ago (Sep 7, 2026) parent 6b215c2

1 changed file +25 −2

modified packages/core/src/ssrf.ts +25 −2
@@ -158,8 +158,7 @@ export async function assertUrlAllowed(raw: string, opts: UrlPolicyOptions = {})
158 158 const t0 = performance.now();
159 159 let records: Array<{ address: string; family: number }>;
160 160 try {
161 const { promises: dns } = await import("node:dns");
162 records = await dns.lookup(hostname, { all: true, verbatim: true });
161 + records = await resolveAll(hostname);
163 162 } catch {
164 163 throw new FetchaError("TARGET_UNAVAILABLE", "The target hostname could not be resolved.");
165 164 }
@@ -172,3 +171,27 @@ export async function assertUrlAllowed(raw: string, opts: UrlPolicyOptions = {})
172 171 }
173 172 return { url, hostname, addresses: records.map((r) => r.address), dns_ms };
174 173 }
174 +
175 +const FALLBACK_DNS_SERVERS = (process.env.FETCHA_DNS_FALLBACK ?? "1.1.1.1,8.8.8.8,9.9.9.9").split(",").map((s) => s.trim()).filter(Boolean);
176 +
177 +/**
178 + * Resolve every address of a hostname. Uses the system resolver first, then falls back to
179 + * public resolvers (some hosts run split-horizon/MagicDNS resolvers that fail on unrelated names).
180 + */
181 +export async function resolveAll(hostname: string): Promise<Array<{ address: string; family: number }>> {
182 + const dnsMod = await import("node:dns");
183 + try {
184 + const recs = await dnsMod.promises.lookup(hostname, { all: true, verbatim: true });
185 + if (recs.length) return recs;
186 + } catch {
187 + /* fall through to public resolvers */
188 + }
189 + const resolver = new dnsMod.promises.Resolver({ timeout: 4000, tries: 2 });
190 + resolver.setServers(FALLBACK_DNS_SERVERS);
191 + const [v4, v6] = await Promise.allSettled([resolver.resolve4(hostname), resolver.resolve6(hostname)]);
192 + const out: Array<{ address: string; family: number }> = [];
193 + if (v4.status === "fulfilled") out.push(...v4.value.map((address) => ({ address, family: 4 })));
194 + if (v6.status === "fulfilled") out.push(...v6.value.map((address) => ({ address, family: 6 })));
195 + if (!out.length) throw new Error(`Unable to resolve ${hostname}`);
196 + return out;
197 +}
175 198