diff options
| author | dakkar <dakkar@thenautilus.net> | 2025-07-28 19:28:01 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2025-07-28 19:28:01 +0000 |
| commit | ce3531ed96da39a4e62cb3651df0b8aee49b912f (patch) | |
| tree | 4def24de3c615351ba6ab86390029b393a81fa2f /packages/backend/src/core/HttpRequestService.ts | |
| parent | merge: Fix Postgres / TypeORM errors *FOR STABLE* (!1192) (diff) | |
| parent | fix DI error in HttpRequestService.ts (diff) | |
| download | sharkey-ce3531ed96da39a4e62cb3651df0b8aee49b912f.tar.gz sharkey-ce3531ed96da39a4e62cb3651df0b8aee49b912f.tar.bz2 sharkey-ce3531ed96da39a4e62cb3651df0b8aee49b912f.zip | |
merge: Improve URL validation *FOR STABLE* (!1191)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1191
Approved-by: Hazelnoot <acomputerdog@gmail.com>
Diffstat (limited to 'packages/backend/src/core/HttpRequestService.ts')
| -rw-r--r-- | packages/backend/src/core/HttpRequestService.ts | 45 |
1 files changed, 38 insertions, 7 deletions
diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 151097095d..bd72fefe4f 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -17,9 +17,9 @@ import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import type { IObject, IObjectWithId } from '@/core/activitypub/type.js'; -import { ApUtilityService } from './activitypub/ApUtilityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import type { Response } from 'node-fetch'; -import type { URL } from 'node:url'; import type { Socket } from 'node:net'; export type HttpRequestSendOptions = { @@ -27,7 +27,27 @@ export type HttpRequestSendOptions = { validators?: ((res: Response) => void)[]; }; -export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined, ip: string, port?: number): boolean { +export async function isPrivateUrl(url: URL, lookup: net.LookupFunction): Promise<boolean> { + const ip = await resolveIp(url, lookup); + return ip.range() !== 'unicast'; +} + +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 ?? []) { @@ -44,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}`)); } } @@ -128,10 +148,16 @@ export class HttpRequestService { */ public readonly httpsAgent: https.Agent; + /** + * Get shared DNS resolver + */ + public readonly lookup: net.LookupFunction; + constructor( @Inject(DI.config) private config: Config, private readonly apUtilityService: ApUtilityService, + private readonly utilityService: UtilityService, ) { const cache = new CacheableLookup({ maxTtl: 3600, // 1hours @@ -139,6 +165,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, @@ -236,8 +264,6 @@ export class HttpRequestService { @bindThis public async getActivityJson(url: string, isLocalAddressAllowed = false, allowAnonymous = false): Promise<IObjectWithId> { - this.apUtilityService.assertApUrl(url); - const res = await this.send(url, { method: 'GET', headers: { @@ -303,6 +329,7 @@ export class HttpRequestService { timeout?: number, size?: number, isLocalAddressAllowed?: boolean, + allowHttp?: boolean, } = {}, extra: HttpRequestSendOptions = { throwErrorWhenResponseNotOk: true, @@ -311,6 +338,10 @@ export class HttpRequestService { ): Promise<Response> { const timeout = args.timeout ?? 5000; + const parsedUrl = new URL(url); + const allowHttp = args.allowHttp || await isPrivateUrl(parsedUrl, this.lookup); + this.utilityService.assertUrl(parsedUrl, allowHttp); + const controller = new AbortController(); setTimeout(() => { controller.abort(); @@ -318,7 +349,7 @@ export class HttpRequestService { const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false; - const res = await fetch(url, { + const res = await fetch(parsedUrl, { method: args.method ?? 'GET', headers: { 'User-Agent': this.config.userAgent, |