resolve domain names when checking for private URLs

This commit is contained in:
Hazelnoot 2025-07-25 16:28:53 -04:00 committed by dakkar
parent 3c59a7ae01
commit 25622b536c
3 changed files with 69 additions and 30 deletions

View file

@ -27,16 +27,27 @@ export type HttpRequestSendOptions = {
validators?: ((res: Response) => void)[];
};
export function isPrivateUrl(url: URL): boolean {
if (!ipaddr.isValid(url.hostname)) {
return false;
}
const ip = ipaddr.parse(url.hostname);
export async function isPrivateUrl(url: URL, lookup: net.LookupFunction): Promise<boolean> {
const ip = await resolveIp(url, lookup);
return ip.range() !== 'unicast';
}
export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined, ip: string, port?: number): boolean {
export async function resolveIp(url: URL, lookup: net.LookupFunction) {
if (ipaddr.isValid(url.hostname)) {
return ipaddr.parse(url.hostname);
}
const resolvedIp = await new Promise<string>((resolve, reject) => {
lookup(url.hostname, {}, (err, address) => {
if (err) reject(err);
else resolve(address as string);
});
});
return ipaddr.parse(resolvedIp);
}
export function isAllowedPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined, ip: string, port?: number): boolean {
const parsedIp = ipaddr.parse(ip);
for (const { cidr, ports } of allowedPrivateNetworks ?? []) {
@ -53,7 +64,7 @@ export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined
export function validateSocketConnect(allowedPrivateNetworks: PrivateNetwork[] | undefined, socket: Socket): void {
const address = socket.remoteAddress;
if (address && ipaddr.isValid(address)) {
if (isPrivateIp(allowedPrivateNetworks, address, socket.remotePort)) {
if (isAllowedPrivateIp(allowedPrivateNetworks, address, socket.remotePort)) {
socket.destroy(new Error(`Blocked address: ${address}`));
}
}
@ -142,6 +153,7 @@ export class HttpRequestService {
private config: Config,
private readonly apUtilityService: ApUtilityService,
private readonly utilityService: UtilityService,
private readonly lookup: net.LookupFunction,
) {
const cache = new CacheableLookup({
maxTtl: 3600, // 1hours
@ -149,6 +161,8 @@ export class HttpRequestService {
lookup: false, // nativeのdns.lookupにfallbackしない
});
this.lookup = cache.lookup as unknown as net.LookupFunction;
const agentOption = {
keepAlive: true,
keepAliveMsecs: 30 * 1000,
@ -321,7 +335,7 @@ export class HttpRequestService {
const timeout = args.timeout ?? 5000;
const parsedUrl = new URL(url);
const allowHttp = args.allowHttp || isPrivateUrl(parsedUrl);
const allowHttp = args.allowHttp || await isPrivateUrl(parsedUrl, this.lookup);
this.utilityService.assertUrl(parsedUrl, allowHttp);
const controller = new AbortController();

View file

@ -56,7 +56,6 @@ import type { ApLoggerService } from '../ApLoggerService.js';
import type { ApImageService } from './ApImageService.js';
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
const nameLength = 128;
const summaryLength = 2048;