From 564c724be11895fa6bc1766aef3a149fae3223e3 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 8 Mar 2023 16:30:26 +0000 Subject: [PATCH] Provides the option for blacklisting IPs. --- .../backend-core/src/blacklist/blacklist.ts | 54 +++++++++++++++++++ packages/backend-core/src/blacklist/index.ts | 1 + .../src/blacklist/tests/blacklist.spec.ts | 46 ++++++++++++++++ packages/backend-core/src/environment.ts | 1 + packages/backend-core/src/index.ts | 1 + packages/server/src/integrations/rest.ts | 4 ++ 6 files changed, 107 insertions(+) create mode 100644 packages/backend-core/src/blacklist/blacklist.ts create mode 100644 packages/backend-core/src/blacklist/index.ts create mode 100644 packages/backend-core/src/blacklist/tests/blacklist.spec.ts diff --git a/packages/backend-core/src/blacklist/blacklist.ts b/packages/backend-core/src/blacklist/blacklist.ts new file mode 100644 index 0000000000..1fbb4683f9 --- /dev/null +++ b/packages/backend-core/src/blacklist/blacklist.ts @@ -0,0 +1,54 @@ +import dns from "dns" +import net from "net" +import env from "../environment" +import { promisify } from "util" + +let blackListArray: string[] | undefined +const performLookup = promisify(dns.lookup) + +async function lookup(address: string): Promise { + if (!net.isIP(address)) { + // need this for URL parsing simply + if (!address.startsWith("http")) { + address = `https://${address}` + } + address = new URL(address).hostname + } + const addresses = await performLookup(address, { + all: true, + }) + return addresses.map(addr => addr.address) +} + +export async function refreshBlacklist() { + const blacklist = env.BLACKLIST_IPS + const list = blacklist?.split(",") || [] + let final: string[] = [] + for (let addr of list) { + const trimmed = addr.trim() + if (!net.isIP(trimmed)) { + const addresses = await lookup(trimmed) + final = final.concat(addresses) + } else { + final.push(trimmed) + } + } + blackListArray = final +} + +export async function isBlacklisted(address: string): Promise { + if (!blackListArray) { + await refreshBlacklist() + } + if (blackListArray?.length === 0) { + return false + } + // no need for DNS + let ips: string[] + if (!net.isIP(address)) { + ips = await lookup(address) + } else { + ips = [address] + } + return !!blackListArray?.find(addr => ips.includes(addr)) +} diff --git a/packages/backend-core/src/blacklist/index.ts b/packages/backend-core/src/blacklist/index.ts new file mode 100644 index 0000000000..b5123eed3e --- /dev/null +++ b/packages/backend-core/src/blacklist/index.ts @@ -0,0 +1 @@ +export * from "./blacklist" diff --git a/packages/backend-core/src/blacklist/tests/blacklist.spec.ts b/packages/backend-core/src/blacklist/tests/blacklist.spec.ts new file mode 100644 index 0000000000..23a8dd1454 --- /dev/null +++ b/packages/backend-core/src/blacklist/tests/blacklist.spec.ts @@ -0,0 +1,46 @@ +import { refreshBlacklist, isBlacklisted } from ".." +import env from "../../environment" + +describe("blacklist", () => { + beforeAll(async () => { + env._set( + "BLACKLIST_IPS", + "www.google.com,192.168.1.1, 1.1.1.1,2.2.2.2/something" + ) + await refreshBlacklist() + }) + + it("should blacklist 192.168.1.1", async () => { + expect(await isBlacklisted("192.168.1.1")).toBe(true) + }) + + it("should allow 192.168.1.2", async () => { + expect(await isBlacklisted("192.168.1.2")).toBe(false) + }) + + it("should blacklist www.google.com", async () => { + expect(await isBlacklisted("www.google.com")).toBe(true) + }) + + it("should handle a complex domain", async () => { + expect( + await isBlacklisted("https://www.google.com/derp/?something=1") + ).toBe(true) + }) + + it("should allow www.microsoft.com", async () => { + expect(await isBlacklisted("www.microsoft.com")).toBe(false) + }) + + it("should blacklist an IP that needed trimming", async () => { + expect(await isBlacklisted("1.1.1.1")).toBe(true) + }) + + it("should blacklist 1.1.1.1/something", async () => { + expect(await isBlacklisted("1.1.1.1/something")).toBe(true) + }) + + it("should blacklist 2.2.2.2", async () => { + expect(await isBlacklisted("2.2.2.2")).toBe(true) + }) +}) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 8dc2cce487..cd10f9eb8a 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -95,6 +95,7 @@ const environment = { SMTP_PORT: parseInt(process.env.SMTP_PORT || ""), SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS, DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING, + BLACKLIST_IPS: process.env.BLACKLIST_IPS, /** * Enable to allow an admin user to login using a password. * This can be useful to prevent lockout when configuring SSO. diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 48569548e3..724ecd21ba 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -25,6 +25,7 @@ export * as locks from "./redis/redlockImpl" export * as utils from "./utils" export * as errors from "./errors" export { default as env } from "./environment" +export * as blacklist from "./blacklist" export { SearchParams } from "./db" // Add context to tenancy for backwards compatibility // only do this for external usages to prevent internal diff --git a/packages/server/src/integrations/rest.ts b/packages/server/src/integrations/rest.ts index c24636a5fd..dc63b961c4 100644 --- a/packages/server/src/integrations/rest.ts +++ b/packages/server/src/integrations/rest.ts @@ -19,6 +19,7 @@ import { formatBytes } from "../utilities" import { performance } from "perf_hooks" import FormData from "form-data" import { URLSearchParams } from "url" +import { blacklist } from "@budibase/backend-core" const BodyTypes = { NONE: "none", @@ -398,6 +399,9 @@ class RestIntegration implements IntegrationBase { this.startTimeMs = performance.now() const url = this.getUrl(path, queryString, pagination, paginationValues) + if (await blacklist.isBlacklisted(url)) { + throw new Error("Cannot connect to URL.") + } const response = await fetch(url, input) return await this.parseResponse(response, pagination) }