UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

83 lines 3.58 kB
/** * Safe Fetch — SSRF-hardened binary download helper. * * Combines: * - `assertSafeUrl` (validates and rejects blocked IPs) * - undici `Agent` with custom `connect.lookup` so the actual connection * uses the IP we validated (closes the DNS-rebinding window where the * resolver returns a public IP for the guard but a private IP for the * real request). * - `readBoundedBuffer` for size cap. * - `redirect: "manual"` so a 3xx → private-IP redirect can't bypass * the guard. * * Use this for **every** download of an external (caller-supplied or * third-party-returned) URL. Direct `fetch(url)` of such URLs is unsafe. * * @module utils/safeFetch */ import { Agent, fetch as undiciFetch } from "undici"; import { readBoundedBuffer } from "./sizeGuard.js"; import { validateAndResolveUrl } from "./ssrfGuard.js"; const DEFAULT_TIMEOUT_MS = 60_000; /** * Build a once-off undici Agent whose connect lookup resolves `hostname` to a * fixed IP. This pins the actual TCP connection to the IP we validated, * removing the DNS-rebinding window. */ function buildPinnedAgent(hostname, ip, family) { return new Agent({ connect: { lookup: (host, _options, callback) => { if (host.toLowerCase() !== hostname.toLowerCase()) { // The host the connect layer asks for differs from the URL host — // this happens for absolute Host headers etc. Reject defensively. callback(new Error(`safeFetch: refusing to resolve "${host}" — expected "${hostname}"`), "", 0); return; } callback(null, ip, family); }, }, }); } /** * Safely download a binary asset from an external URL. * * @throws {Error} if the URL is unsafe, the response is too large, a redirect * is encountered, or the HTTP status indicates failure. */ export async function safeDownload(url, options) { const { url: validatedUrl, ip, family } = await validateAndResolveUrl(url); const parsed = new URL(validatedUrl); const hostname = parsed.hostname.replace(/^\[|\]$/g, ""); const agent = buildPinnedAgent(hostname, ip, family); const timeoutCtrl = new AbortController(); const timeoutId = setTimeout(() => timeoutCtrl.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS); const composedSignal = options.signal ? AbortSignal.any([options.signal, timeoutCtrl.signal]) : timeoutCtrl.signal; let response; try { response = await undiciFetch(validatedUrl, { method: "GET", signal: composedSignal, redirect: "manual", // a 3xx → private-IP redirect would bypass the guard dispatcher: agent, }); } finally { clearTimeout(timeoutId); // Close the per-request agent so the pinned connection isn't pooled. agent.close().catch(() => undefined); } if (response.status >= 300 && response.status < 400) { throw new Error(`safeDownload(${options.label}): refused to follow redirect ${response.status}${response.headers.get("location") ?? "<no-location>"} (for ${url})`); } if (!response.ok) { throw new Error(`safeDownload(${options.label}) failed: HTTP ${response.status} for ${url}`); } // readBoundedBuffer expects a Response that exposes Content-Length and // arrayBuffer(). undici Response satisfies both. return readBoundedBuffer(response, options.maxBytes, options.label); } //# sourceMappingURL=safeFetch.js.map