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)[];
|
validators?: ((res: Response) => void)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isPrivateUrl(url: URL): boolean {
|
export async function isPrivateUrl(url: URL, lookup: net.LookupFunction): Promise<boolean> {
|
||||||
if (!ipaddr.isValid(url.hostname)) {
|
const ip = await resolveIp(url, lookup);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ip = ipaddr.parse(url.hostname);
|
|
||||||
return ip.range() !== 'unicast';
|
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);
|
const parsedIp = ipaddr.parse(ip);
|
||||||
|
|
||||||
for (const { cidr, ports } of allowedPrivateNetworks ?? []) {
|
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 {
|
export function validateSocketConnect(allowedPrivateNetworks: PrivateNetwork[] | undefined, socket: Socket): void {
|
||||||
const address = socket.remoteAddress;
|
const address = socket.remoteAddress;
|
||||||
if (address && ipaddr.isValid(address)) {
|
if (address && ipaddr.isValid(address)) {
|
||||||
if (isPrivateIp(allowedPrivateNetworks, address, socket.remotePort)) {
|
if (isAllowedPrivateIp(allowedPrivateNetworks, address, socket.remotePort)) {
|
||||||
socket.destroy(new Error(`Blocked address: ${address}`));
|
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,6 +153,7 @@ export class HttpRequestService {
|
||||||
private config: Config,
|
private config: Config,
|
||||||
private readonly apUtilityService: ApUtilityService,
|
private readonly apUtilityService: ApUtilityService,
|
||||||
private readonly utilityService: UtilityService,
|
private readonly utilityService: UtilityService,
|
||||||
|
private readonly lookup: net.LookupFunction,
|
||||||
) {
|
) {
|
||||||
const cache = new CacheableLookup({
|
const cache = new CacheableLookup({
|
||||||
maxTtl: 3600, // 1hours
|
maxTtl: 3600, // 1hours
|
||||||
|
@ -149,6 +161,8 @@ export class HttpRequestService {
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.lookup = cache.lookup as unknown as net.LookupFunction;
|
||||||
|
|
||||||
const agentOption = {
|
const agentOption = {
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
|
@ -321,7 +335,7 @@ export class HttpRequestService {
|
||||||
const timeout = args.timeout ?? 5000;
|
const timeout = args.timeout ?? 5000;
|
||||||
|
|
||||||
const parsedUrl = new URL(url);
|
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);
|
this.utilityService.assertUrl(parsedUrl, allowHttp);
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
|
@ -7,8 +7,9 @@ import { describe, jest } from '@jest/globals';
|
||||||
import type { Mock } from 'jest-mock';
|
import type { Mock } from 'jest-mock';
|
||||||
import type { PrivateNetwork } from '@/config.js';
|
import type { PrivateNetwork } from '@/config.js';
|
||||||
import type { Socket } from 'net';
|
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 { parsePrivateNetworks } from '@/config.js';
|
||||||
|
import { IPv4 } from 'ipaddr.js';
|
||||||
|
|
||||||
describe(HttpRequestService, () => {
|
describe(HttpRequestService, () => {
|
||||||
let allowedPrivateNetworks: PrivateNetwork[] | undefined;
|
let allowedPrivateNetworks: PrivateNetwork[] | undefined;
|
||||||
|
@ -21,56 +22,81 @@ describe(HttpRequestService, () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isPrivateIp', () => {
|
describe(isAllowedPrivateIp, () => {
|
||||||
it('should return false when ip public', () => {
|
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();
|
expect(result).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when ip private and port matches', () => {
|
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();
|
expect(result).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when ip private and all ports undefined', () => {
|
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();
|
expect(result).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when ip private and no ports specified', () => {
|
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();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when ip private and port does not match', () => {
|
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();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when ip private and port is null but ports are specified', () => {
|
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();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isPrivateUrl', () => {
|
const fakeLookup = (host: string, _: unknown, callback: (err: Error | null, ip: string) => void) => {
|
||||||
it('should return false when URL is not an IP', () => {
|
if (host === 'localhost') {
|
||||||
const result = isPrivateUrl(new URL('https://example.com'));
|
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);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when IP is public', () => {
|
it('should return true when URL is private host', async () => {
|
||||||
const result = isPrivateUrl(new URL('https://23.192.228.80'));
|
const result = await isPrivateUrl(new URL('https://localhost'), fakeLookup);
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true when IP is private', () => {
|
|
||||||
const result = isPrivateUrl(new URL('https://127.0.0.1'));
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when IP is private with port and path', () => {
|
it('should return false when IP is public', async () => {
|
||||||
const result = isPrivateUrl(new URL('https://127.0.0.1:443/some/path'));
|
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);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue