From fb63167d854f277ffeb82da720580b3e99f9cc8c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 11 May 2025 23:22:41 -0400 Subject: [PATCH 1/7] allow private IP ranges to specify allowed ports --- packages/backend/src/config.ts | 33 +++++- .../backend/src/core/HttpRequestService.ts | 68 +++++------- .../test/unit/core/HttpRequestService.ts | 103 ++++++++++++++++++ 3 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 packages/backend/test/unit/core/HttpRequestService.ts diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 92fc2b8a13..2a3184f9b4 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -8,9 +8,11 @@ import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; import { globSync } from 'glob'; +import ipaddr from 'ipaddr.js'; import type * as Sentry from '@sentry/node'; import type * as SentryVue from '@sentry/vue'; import type { RedisOptions } from 'ioredis'; +import type { IPv4, IPv6 } from 'ipaddr.js'; type RedisOptionsSource = Partial & { host?: string; @@ -152,6 +154,33 @@ type Source = { } }; +export type PrivateNetwork = { + /** + * CIDR IP/netmask definition of the IP range to match. + */ + cidr: [ip: IPv4 | IPv6, mask: number]; + + /** + * List of ports to match. + * If undefined, then all ports match. + * If empty, then NO ports match. + */ + ports?: number[]; +}; + +export function parsePrivateNetworks(patterns: string[]): PrivateNetwork[]; +export function parsePrivateNetworks(patterns: undefined): undefined; +export function parsePrivateNetworks(patterns: string[] | undefined): PrivateNetwork[] | undefined; +export function parsePrivateNetworks(patterns: string[] | undefined): PrivateNetwork[] | undefined { + return patterns?.map(e => { + const [ip, ports] = e.split('#') as [string, ...(string | undefined)[]]; + return { + cidr: ipaddr.parseCIDR(ip), + ports: ports?.split(',').map(p => parseInt(p)), + }; + }); +} + export type Config = { url: string; port: number; @@ -190,7 +219,7 @@ export type Config = { proxy: string | undefined; proxySmtp: string | undefined; proxyBypassHosts: string[] | undefined; - allowedPrivateNetworks: string[] | undefined; + allowedPrivateNetworks: PrivateNetwork[] | undefined; disallowExternalApRedirect: boolean; maxFileSize: number; maxNoteLength: number; @@ -382,7 +411,7 @@ export function loadConfig(): Config { proxy: config.proxy, proxySmtp: config.proxySmtp, proxyBypassHosts: config.proxyBypassHosts, - allowedPrivateNetworks: config.allowedPrivateNetworks, + allowedPrivateNetworks: parsePrivateNetworks(config.allowedPrivateNetworks), disallowExternalApRedirect: config.disallowExternalApRedirect ?? false, maxFileSize: config.maxFileSize ?? 262144000, maxNoteLength: config.maxNoteLength ?? 3000, diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 12047346fb..7c086c9976 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -12,7 +12,7 @@ import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +import type { Config, PrivateNetwork } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; @@ -20,12 +20,36 @@ import type { IObject, IObjectWithId } from '@/core/activitypub/type.js'; import { ApUtilityService } from './activitypub/ApUtilityService.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; +import type { Socket } from 'node:net'; export type HttpRequestSendOptions = { throwErrorWhenResponseNotOk: boolean; validators?: ((res: Response) => void)[]; }; +export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined, ip: string, port?: number): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const { cidr, ports } of allowedPrivateNetworks ?? []) { + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(cidr)) { + if (port == null || ports == null || ports.includes(port)) { + return false; + } + } + } + + return parsedIp.range() !== 'unicast'; +} + +export function validateSocketConnect(allowedPrivateNetworks: PrivateNetwork[] | undefined, socket: Socket): void { + const address = socket.remoteAddress; + if (address && ipaddr.isValid(address)) { + if (isPrivateIp(allowedPrivateNetworks, address, socket.remotePort)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } +} + declare module 'node:http' { interface Agent { createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket; @@ -44,31 +68,12 @@ class HttpRequestServiceAgent extends http.Agent { public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { const socket = super.createConnection(options, callback) .on('connect', () => { - const address = socket.remoteAddress; if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } - } + validateSocketConnect(this.config.allowedPrivateNetworks, socket); } }); 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'; - } } class HttpsRequestServiceAgent extends https.Agent { @@ -83,31 +88,12 @@ class HttpsRequestServiceAgent extends https.Agent { public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { const socket = super.createConnection(options, callback) .on('connect', () => { - const address = socket.remoteAddress; if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } - } + validateSocketConnect(this.config.allowedPrivateNetworks, socket); } }); 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() diff --git a/packages/backend/test/unit/core/HttpRequestService.ts b/packages/backend/test/unit/core/HttpRequestService.ts new file mode 100644 index 0000000000..3185b91567 --- /dev/null +++ b/packages/backend/test/unit/core/HttpRequestService.ts @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import type { Mock } from 'jest-mock'; +import type { PrivateNetwork } from '@/config.js'; +import type { Socket } from 'net'; +import { HttpRequestService, isPrivateIp, validateSocketConnect } from '@/core/HttpRequestService.js'; +import { parsePrivateNetworks } from '@/config.js'; + +describe(HttpRequestService, () => { + let allowedPrivateNetworks: PrivateNetwork[] | undefined; + + beforeEach(() => { + allowedPrivateNetworks = parsePrivateNetworks([ + '10.0.0.1/32', + '127.0.0.1/32#1', + '127.0.0.1/32#3,4,5', + ]); + }); + + describe('isPrivateIp', () => { + it('should return false when ip public', () => { + const result = isPrivateIp(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); + expect(result).toBeFalsy(); + }); + + it('should return true when ip private and no ports specified', () => { + const result = isPrivateIp(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); + expect(result).toBeTruthy(); + }); + }); + + describe('validateSocketConnect', () => { + let fakeSocket: Socket; + let fakeSocketMutable: { + remoteAddress: string | undefined; + remotePort: number | undefined; + destroy: Mock<(error?: Error) => void>; + }; + + beforeEach(() => { + fakeSocketMutable = { + remoteAddress: '74.125.127.100', + remotePort: 80, + destroy: jest.fn<(error?: Error) => void>(), + }; + fakeSocket = fakeSocketMutable as unknown as Socket; + }); + + it('should accept when IP is empty', () => { + fakeSocketMutable.remoteAddress = undefined; + + validateSocketConnect(allowedPrivateNetworks, fakeSocket); + + expect(fakeSocket.destroy).not.toHaveBeenCalled(); + }); + + it('should accept when IP is invalid', () => { + fakeSocketMutable.remoteAddress = 'AB939ajd9jdajsdja8jj'; + + validateSocketConnect(allowedPrivateNetworks, fakeSocket); + + expect(fakeSocket.destroy).not.toHaveBeenCalled(); + }); + + it('should accept when IP is valid', () => { + validateSocketConnect(allowedPrivateNetworks, fakeSocket); + + expect(fakeSocket.destroy).not.toHaveBeenCalled(); + }); + + it('should accept when IP is private and port match', () => { + fakeSocketMutable.remoteAddress = '127.0.0.1'; + fakeSocketMutable.remotePort = 1; + + validateSocketConnect(allowedPrivateNetworks, fakeSocket); + + expect(fakeSocket.destroy).not.toHaveBeenCalled(); + }); + + it('should reject when IP is private and port no match', () => { + fakeSocketMutable.remoteAddress = '127.0.0.1'; + fakeSocketMutable.remotePort = 2; + + validateSocketConnect(allowedPrivateNetworks, fakeSocket); + + expect(fakeSocket.destroy).toHaveBeenCalled(); + }); + }); +}); From 5116586d79df7216b124e74715f6414ffffa7e3a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 13 May 2025 22:19:24 -0400 Subject: [PATCH 2/7] improve YAML syntax for defining allowed IPs --- .config/ci.yml | 21 ++++++++++++-- .config/cypress-devcontainer.yml | 24 +++++++++++++-- .config/docker_example.yml | 21 ++++++++++++-- .config/example.yml | 21 ++++++++++++-- packages/backend/src/config.ts | 50 ++++++++++++++++++++++++-------- 5 files changed, 113 insertions(+), 24 deletions(-) diff --git a/.config/ci.yml b/.config/ci.yml index fefa45643c..4a6d21e1d5 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -321,9 +321,24 @@ attachLdSignatureForRelays: true # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). -#allowedPrivateNetworks: [ -# '127.0.0.1/32' -#] +# Some example configurations: +#allowedPrivateNetworks: +# # Allow connections to 127.0.0.1 on any port +# - '127.0.0.1/32' +# # Allow connections to 127.0.0.* on any port +# - '127.0.0.1/24' +# # Allow connections to 127.0.0.1 on any port +# - '127.0.0.1' +# # Allow connections to 127.0.0.1 on any port +# - network: '127.0.0.1' +# # Allow connections to 127.0.0.1 on port 80 +# - network: '127.0.0.1' +# ports: [80] +# # Allow connections to 127.0.0.1 on port 80 or 443 +# - network: '127.0.0.1' +# ports: +# - 80 +# - 443 #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index e4eb8cc805..356d583611 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -269,9 +269,27 @@ proxyRemoteFiles: true # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true -allowedPrivateNetworks: [ - '127.0.0.1/32' -] +# For security reasons, uploading attachments from the intranet is prohibited, +# but exceptions can be made from the following settings. Default value is "undefined". +# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). +# Some example configurations: +allowedPrivateNetworks: + # Allow connections to 127.0.0.1 on any port + - '127.0.0.1/32' +# # Allow connections to 127.0.0.* on any port +# - '127.0.0.1/24' +# # Allow connections to 127.0.0.1 on any port +# - '127.0.0.1' +# # Allow connections to 127.0.0.1 on any port +# - network: '127.0.0.1' +# # Allow connections to 127.0.0.1 on port 80 +# - network: '127.0.0.1' +# ports: [80] +# # Allow connections to 127.0.0.1 on port 80 or 443 +# - network: '127.0.0.1' +# ports: +# - 80 +# - 443 # Disable automatic redirect for ActivityPub object lookup. (default: false) # This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 7968a7d1f4..68679f64ed 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -378,9 +378,24 @@ attachLdSignatureForRelays: true # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). -#allowedPrivateNetworks: [ -# '127.0.0.1/32' -#] +# Some example configurations: +#allowedPrivateNetworks: +# # Allow connections to 127.0.0.1 on any port +# - '127.0.0.1/32' +# # Allow connections to 127.0.0.* on any port +# - '127.0.0.1/24' +# # Allow connections to 127.0.0.1 on any port +# - '127.0.0.1' +# # Allow connections to 127.0.0.1 on any port +# - network: '127.0.0.1' +# # Allow connections to 127.0.0.1 on port 80 +# - network: '127.0.0.1' +# ports: [80] +# # Allow connections to 127.0.0.1 on port 80 or 443 +# - network: '127.0.0.1' +# ports: +# - 80 +# - 443 #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] diff --git a/.config/example.yml b/.config/example.yml index d0ed4defaa..9cb1e656c1 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -381,9 +381,24 @@ attachLdSignatureForRelays: true # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). -#allowedPrivateNetworks: [ -# '127.0.0.1/32' -#] +# Some example configurations: +#allowedPrivateNetworks: +# # Allow connections to 127.0.0.1 on any port +# - '127.0.0.1/32' +# # Allow connections to 127.0.0.* on any port +# - '127.0.0.1/24' +# # Allow connections to 127.0.0.1 on any port +# - '127.0.0.1' +# # Allow connections to 127.0.0.1 on any port +# - network: '127.0.0.1' +# # Allow connections to 127.0.0.1 on port 80 +# - network: '127.0.0.1' +# ports: [80] +# # Allow connections to 127.0.0.1 on port 80 or 443 +# - network: '127.0.0.1' +# ports: +# - 80 +# - 443 #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 2a3184f9b4..9725bcc367 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -84,7 +84,7 @@ type Source = { proxySmtp?: string; proxyBypassHosts?: string[]; - allowedPrivateNetworks?: string[]; + allowedPrivateNetworks?: PrivateNetworkSource[]; disallowExternalApRedirect?: boolean; maxFileSize?: number; @@ -154,11 +154,13 @@ type Source = { } }; +export type PrivateNetworkSource = string | { ip?: string, ports?: number[] }; + export type PrivateNetwork = { /** * CIDR IP/netmask definition of the IP range to match. */ - cidr: [ip: IPv4 | IPv6, mask: number]; + cidr: CIDR; /** * List of ports to match. @@ -168,17 +170,41 @@ export type PrivateNetwork = { ports?: number[]; }; -export function parsePrivateNetworks(patterns: string[]): PrivateNetwork[]; +export type CIDR = [ip: IPv4 | IPv6, mask: number]; + +export function parsePrivateNetworks(patterns: PrivateNetworkSource[]): PrivateNetwork[]; export function parsePrivateNetworks(patterns: undefined): undefined; -export function parsePrivateNetworks(patterns: string[] | undefined): PrivateNetwork[] | undefined; -export function parsePrivateNetworks(patterns: string[] | undefined): PrivateNetwork[] | undefined { - return patterns?.map(e => { - const [ip, ports] = e.split('#') as [string, ...(string | undefined)[]]; - return { - cidr: ipaddr.parseCIDR(ip), - ports: ports?.split(',').map(p => parseInt(p)), - }; - }); +export function parsePrivateNetworks(patterns: PrivateNetworkSource[] | undefined): PrivateNetwork[] | undefined; +export function parsePrivateNetworks(patterns: PrivateNetworkSource[] | undefined): PrivateNetwork[] | undefined { + if (!patterns) return undefined; + return patterns + .map(e => { + if (typeof(e) === 'string') { + const cidr = parseIpOrMask(e); + if (cidr) { + return { cidr } satisfies PrivateNetwork; + } + } else if (e.ip) { + const cidr = parseIpOrMask(e.ip); + if (cidr) { + return { cidr, ports: e.ports } satisfies PrivateNetwork; + } + } + + console.warn('[config] Skipping invalid entry in allowedPrivateNetworks: ', e); + return null; + }) + .filter(p => p != null); +} + +function parseIpOrMask(ipOrMask: string): CIDR | null { + if (ipaddr.isValidCIDR(ipOrMask)) { + return ipaddr.parseCIDR(ipOrMask); + } + if (ipaddr.isValid(ipOrMask)) { + return ipaddr.parseCIDR(ipOrMask); + } + return null; } export type Config = { From 4ddb16aa9a216141b14ecbbc0c348075e9d3db4a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 13 May 2025 22:21:03 -0400 Subject: [PATCH 3/7] rename "mask" to "prefixLength" for clarity --- packages/backend/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 9725bcc367..31cb567d0e 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -170,7 +170,7 @@ export type PrivateNetwork = { ports?: number[]; }; -export type CIDR = [ip: IPv4 | IPv6, mask: number]; +export type CIDR = [ip: IPv4 | IPv6, prefixLength: number]; export function parsePrivateNetworks(patterns: PrivateNetworkSource[]): PrivateNetwork[]; export function parsePrivateNetworks(patterns: undefined): undefined; From ebd4ccdd55a509e02fd8964061b90361d6c93924 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 13 May 2025 22:22:40 -0400 Subject: [PATCH 4/7] enforce port restrictions against requests that happen to be missing the port --- packages/backend/src/core/HttpRequestService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 7c086c9976..2951691129 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -32,7 +32,7 @@ export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined for (const { cidr, ports } of allowedPrivateNetworks ?? []) { if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(cidr)) { - if (port == null || ports == null || ports.includes(port)) { + if (ports == null || (port != null && ports.includes(port))) { return false; } } From e914be3694966ce8adf613ddc6ce957111611b7b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 13 May 2025 22:24:48 -0400 Subject: [PATCH 5/7] fix "network" property in allowedPrivateNetworks --- packages/backend/src/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 31cb567d0e..21250df29e 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -154,7 +154,7 @@ type Source = { } }; -export type PrivateNetworkSource = string | { ip?: string, ports?: number[] }; +export type PrivateNetworkSource = string | { network?: string, ports?: number[] }; export type PrivateNetwork = { /** @@ -184,8 +184,8 @@ export function parsePrivateNetworks(patterns: PrivateNetworkSource[] | undefine if (cidr) { return { cidr } satisfies PrivateNetwork; } - } else if (e.ip) { - const cidr = parseIpOrMask(e.ip); + } else if (e.network) { + const cidr = parseIpOrMask(e.network); if (cidr) { return { cidr, ports: e.ports } satisfies PrivateNetwork; } From b888ff931d0eb76b0c03e606e9002c4f6d9197cb Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 13 May 2025 22:27:18 -0400 Subject: [PATCH 6/7] fix unit tests for HttpRequestService.ts --- .../backend/test/unit/core/HttpRequestService.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/backend/test/unit/core/HttpRequestService.ts b/packages/backend/test/unit/core/HttpRequestService.ts index 3185b91567..a2f4604e7b 100644 --- a/packages/backend/test/unit/core/HttpRequestService.ts +++ b/packages/backend/test/unit/core/HttpRequestService.ts @@ -16,8 +16,8 @@ describe(HttpRequestService, () => { beforeEach(() => { allowedPrivateNetworks = parsePrivateNetworks([ '10.0.0.1/32', - '127.0.0.1/32#1', - '127.0.0.1/32#3,4,5', + { network: '127.0.0.1/32', ports: [1] }, + { network: '127.0.0.1/32', ports: [3, 4, 5] }, ]); }); @@ -32,6 +32,11 @@ describe(HttpRequestService, () => { expect(result).toBeFalsy(); }); + it('should return false when ip private and all ports undefined', () => { + const result = isPrivateIp(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); expect(result).toBeTruthy(); @@ -41,6 +46,11 @@ describe(HttpRequestService, () => { const result = isPrivateIp(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); + expect(result).toBeTruthy(); + }); }); describe('validateSocketConnect', () => { From dc79244f5b8bc8d5ed413dfe417b584bfc39099a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 13 May 2025 22:28:19 -0400 Subject: [PATCH 7/7] fix parsing bare IPs --- packages/backend/src/config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 21250df29e..a48fa7e646 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -202,7 +202,8 @@ function parseIpOrMask(ipOrMask: string): CIDR | null { return ipaddr.parseCIDR(ipOrMask); } if (ipaddr.isValid(ipOrMask)) { - return ipaddr.parseCIDR(ipOrMask); + const ip = ipaddr.parse(ipOrMask); + return [ip, 32]; } return null; }