From f36f4b5398561dcf7365729c530f5b1868c6b994 Mon Sep 17 00:00:00 2001 From: rectcoordsystem Date: Wed, 6 Nov 2024 05:31:11 +0900 Subject: fix(backend): check target IP before sending HTTP request --- packages/backend/src/core/HttpRequestService.ts | 90 ++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/core/HttpRequestService.ts') diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 08e9f46b2d..6c013eacc0 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -6,6 +6,7 @@ import * as http from 'node:http'; import * as https from 'node:https'; import * as net from 'node:net'; +import ipaddr from 'ipaddr.js'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; @@ -25,6 +26,91 @@ export type HttpRequestSendOptions = { validators?: ((res: Response) => void)[]; }; +@Injectable() +class HttpRequestServiceAgent extends http.Agent { + constructor( + @Inject(DI.config) + private config: Config, + + options?: Object + ) { + super(options); + } + + @bindThis + public createConnection(options: Object, callback?: Function): net.Socket { + const socket = super.createConnection(options, callback) + .on('connect', ()=>{ + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } + } + }); + return socket; + }; + + @bindThis + private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const net of this.config.allowedPrivateNetworks ?? []) { + const cidr = ipaddr.parseCIDR(net); + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { + return false; + } + } + + return parsedIp.range() !== 'unicast'; + } +} + +@Injectable() +class HttpsRequestServiceAgent extends https.Agent { + constructor( + @Inject(DI.config) + private config: Config, + + options?: Object + ) { + super(options); + } + + @bindThis + public createConnection(options: Object, callback?: Function): net.Socket { + const socket = super.createConnection(options, callback) + .on('connect', ()=>{ + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } + } + }); + return socket; + }; + + @bindThis + private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const net of this.config.allowedPrivateNetworks ?? []) { + const cidr = ipaddr.parseCIDR(net); + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { + return false; + } + } + + return parsedIp.range() !== 'unicast'; + } +} + + @Injectable() export class HttpRequestService { /** @@ -57,14 +143,14 @@ export class HttpRequestService { lookup: false, // nativeのdns.lookupにfallbackしない }); - this.http = new http.Agent({ + this.http = new HttpRequestServiceAgent(config, { keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup as unknown as net.LookupFunction, localAddress: config.outgoingAddress, }); - this.https = new https.Agent({ + this.https = new HttpsRequestServiceAgent(config, { keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup as unknown as net.LookupFunction, -- cgit v1.2.3-freya