mirror of
https://activitypub.software/TransFem-org/Sharkey.git
synced 2025-08-31 22:50:43 +00:00
resolve domain names when checking for private URLs
This commit is contained in:
parent
63bac24ece
commit
05bc6f5d86
2 changed files with 69 additions and 29 deletions
|
@ -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();
|
||||
|
|
|
@ -7,8 +7,9 @@ import { describe, jest } from '@jest/globals';
|
|||
import type { Mock } from 'jest-mock';
|
||||
import type { PrivateNetwork } from '@/config.js';
|
||||
import type { Socket } from 'net';
|
||||
import { HttpRequestService, isPrivateIp, isPrivateUrl, validateSocketConnect } from '@/core/HttpRequestService.js';
|
||||
import { HttpRequestService, isAllowedPrivateIp, isPrivateUrl, resolveIp, validateSocketConnect } from '@/core/HttpRequestService.js';
|
||||
import { parsePrivateNetworks } from '@/config.js';
|
||||
import { IPv4 } from 'ipaddr.js';
|
||||
|
||||
describe(HttpRequestService, () => {
|
||||
let allowedPrivateNetworks: PrivateNetwork[] | undefined;
|
||||
|
@ -21,56 +22,81 @@ describe(HttpRequestService, () => {
|
|||
]);
|
||||
});
|
||||
|
||||
describe('isPrivateIp', () => {
|
||||
describe(isAllowedPrivateIp, () => {
|
||||
it('should return false when ip public', () => {
|
||||
const result = isPrivateIp(allowedPrivateNetworks, '74.125.127.100', 80);
|
||||
const result = isAllowedPrivateIp(allowedPrivateNetworks, '74.125.127.100', 80);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when ip private and port matches', () => {
|
||||
const result = isPrivateIp(allowedPrivateNetworks, '127.0.0.1', 1);
|
||||
const result = isAllowedPrivateIp(allowedPrivateNetworks, '127.0.0.1', 1);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when ip private and all ports undefined', () => {
|
||||
const result = isPrivateIp(allowedPrivateNetworks, '10.0.0.1', undefined);
|
||||
const result = isAllowedPrivateIp(allowedPrivateNetworks, '10.0.0.1', undefined);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true when ip private and no ports specified', () => {
|
||||
const result = isPrivateIp(allowedPrivateNetworks, '10.0.0.2', 80);
|
||||
const result = isAllowedPrivateIp(allowedPrivateNetworks, '10.0.0.2', 80);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when ip private and port does not match', () => {
|
||||
const result = isPrivateIp(allowedPrivateNetworks, '127.0.0.1', 80);
|
||||
const result = isAllowedPrivateIp(allowedPrivateNetworks, '127.0.0.1', 80);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when ip private and port is null but ports are specified', () => {
|
||||
const result = isPrivateIp(allowedPrivateNetworks, '127.0.0.1', undefined);
|
||||
const result = isAllowedPrivateIp(allowedPrivateNetworks, '127.0.0.1', undefined);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPrivateUrl', () => {
|
||||
it('should return false when URL is not an IP', () => {
|
||||
const result = isPrivateUrl(new URL('https://example.com'));
|
||||
const fakeLookup = (host: string, _: unknown, callback: (err: Error | null, ip: string) => void) => {
|
||||
if (host === 'localhost') {
|
||||
callback(null, '127.0.0.1');
|
||||
} else {
|
||||
callback(null, '23.192.228.80');
|
||||
}
|
||||
};
|
||||
|
||||
describe(resolveIp, () => {
|
||||
it('should parse inline IPs', async () => {
|
||||
const result = await resolveIp(new URL('https://10.0.0.1'), fakeLookup);
|
||||
expect(result.toString()).toEqual('10.0.0.1');
|
||||
});
|
||||
|
||||
it('should resolve domain names', async () => {
|
||||
const result = await resolveIp(new URL('https://localhost'), fakeLookup);
|
||||
expect(result.toString()).toEqual('127.0.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe(isPrivateUrl, () => {
|
||||
it('should return false when URL is public host', async () => {
|
||||
const result = await isPrivateUrl(new URL('https://example.com'), fakeLookup);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when IP is public', () => {
|
||||
const result = isPrivateUrl(new URL('https://23.192.228.80'));
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when IP is private', () => {
|
||||
const result = isPrivateUrl(new URL('https://127.0.0.1'));
|
||||
it('should return true when URL is private host', async () => {
|
||||
const result = await isPrivateUrl(new URL('https://localhost'), fakeLookup);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when IP is private with port and path', () => {
|
||||
const result = isPrivateUrl(new URL('https://127.0.0.1:443/some/path'));
|
||||
it('should return false when IP is public', async () => {
|
||||
const result = await isPrivateUrl(new URL('https://23.192.228.80'), fakeLookup);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when IP is private', async () => {
|
||||
const result = await isPrivateUrl(new URL('https://127.0.0.1'), fakeLookup);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when IP is private with port and path', async () => {
|
||||
const result = await isPrivateUrl(new URL('https://127.0.0.1:443/some/path'), fakeLookup);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue