diff --git a/plugin-server/src/utils/fetch.ts b/plugin-server/src/utils/fetch.ts index 62da5b046a2..96358d8ec28 100644 --- a/plugin-server/src/utils/fetch.ts +++ b/plugin-server/src/utils/fetch.ts @@ -2,13 +2,38 @@ import { LookupAddress } from 'dns' import dns from 'dns/promises' +import http from 'http' +import https from 'https' import * as ipaddr from 'ipaddr.js' +import net from 'node:net' import fetch, { type RequestInfo, type RequestInit, type Response, FetchError, Request } from 'node-fetch' import { URL } from 'url' import { runInSpan } from '../sentry' import { isProdEnv } from './env-utils' +const staticLookup: net.LookupFunction = async (hostname, options, cb) => { + let addrinfo: LookupAddress[] + try { + addrinfo = await dns.lookup(hostname, { all: true }) + } catch (err) { + cb(new Error('Invalid hostname'), '', 4) + return + } + for (const { address } of addrinfo) { + // Prevent addressing internal services + if (ipaddr.parse(address).range() !== 'unicast') { + cb(new Error('Internal hostname'), '', 4) + return + } + } + if (addrinfo.length === 0) { + cb(new Error(`Unable to resolve ${hostname}`), '', 4) + return + } + cb(null, addrinfo[0].address, addrinfo[0].family) +} + export async function trackedFetch(url: RequestInfo, init?: RequestInit): Promise { const request = new Request(url, init) return await runInSpan( @@ -19,6 +44,13 @@ export async function trackedFetch(url: RequestInfo, init?: RequestInit): Promis async () => { if (isProdEnv() && !process.env.NODE_ENV?.includes('functional-tests')) { await raiseIfUserProvidedUrlUnsafe(request.url) + return await fetch(url, { + ...init, + agent: ({ protocol }: URL) => + protocol === 'http:' + ? new http.Agent({ lookup: staticLookup }) + : new https.Agent({ lookup: staticLookup }), + }) } return await fetch(url, init) }