From b3a658d8cd6e2cac5125a60d73ba4622e2be1831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 27 Jan 2025 17:20:31 +0100 Subject: [PATCH 01/12] Implement ip allowlist (e.g. geo based) --- library/agent/Agent.test.ts | 38 ++++++++++ library/agent/Agent.ts | 18 +++-- library/agent/ServiceConfig.test.ts | 69 ++++++++++++++----- library/agent/ServiceConfig.ts | 37 ++++++++-- library/agent/api/fetchBlockedLists.ts | 12 +++- .../getRateLimitedEndpoint.test.ts | 11 ++- library/sources/HTTPServer.test.ts | 4 +- library/sources/Hono.test.ts | 39 +++++++++++ .../http-server/checkIfRequestIsBlocked.ts | 18 +++++ 9 files changed, 215 insertions(+), 31 deletions(-) diff --git a/library/agent/Agent.test.ts b/library/agent/Agent.test.ts index 358b9fc6b..daf5910e8 100644 --- a/library/agent/Agent.test.ts +++ b/library/agent/Agent.test.ts @@ -19,6 +19,8 @@ import { Context } from "./Context"; import { createTestAgent } from "../helpers/createTestAgent"; import { setTimeout } from "node:timers/promises"; +let shouldOnlyAllowSomeIPAddresses = false; + wrap(fetch, "fetch", function mock() { return async function mock() { return { @@ -32,6 +34,15 @@ wrap(fetch, "fetch", function mock() { }, ], blockedUserAgents: "AI2Bot|Bytespider", + allowedIPAddresses: shouldOnlyAllowSomeIPAddresses + ? [ + { + source: "name", + description: "Description", + ips: ["4.3.2.1"], + }, + ] + : [], }), }; }; @@ -1099,6 +1110,8 @@ t.test("it does not fetch blocked IPs if serverless", async () => { blocked: false, }); + t.same(agent.getConfig().shouldOnlyAllowSomeIPAddresses(), false); + t.same( agent .getConfig() @@ -1110,3 +1123,28 @@ t.test("it does not fetch blocked IPs if serverless", async () => { } ); }); + +t.test("it only allows some IP addresses", async () => { + shouldOnlyAllowSomeIPAddresses = true; + const agent = createTestAgent({ + token: new Token("123"), + suppressConsoleLog: false, + }); + + agent.start([]); + + await setTimeout(0); + + t.same(agent.getConfig().isIPAddressBlocked("1.3.2.4"), { + blocked: true, + reason: "Description", + }); + t.same(agent.getConfig().isIPAddressBlocked("fe80::1234:5678:abcd:ef12"), { + blocked: true, + reason: "Description", + }); + + t.same(agent.getConfig().shouldOnlyAllowSomeIPAddresses(), true); + t.same(agent.getConfig().isOnlyAllowedIPAddress("1.2.3.4"), false); + t.same(agent.getConfig().isOnlyAllowedIPAddress("4.3.2.1"), true); +}); diff --git a/library/agent/Agent.ts b/library/agent/Agent.ts index 99ec82aae..1aa627b3d 100644 --- a/library/agent/Agent.ts +++ b/library/agent/Agent.ts @@ -41,7 +41,15 @@ export class Agent { private timeoutInMS = 10000; private hostnames = new Hostnames(200); private users = new Users(1000); - private serviceConfig = new ServiceConfig([], Date.now(), [], [], true, []); + private serviceConfig = new ServiceConfig( + [], + Date.now(), + [], + [], + true, + [], + [] + ); private routes: Routes = new Routes(200); private rateLimiter: RateLimiter = new RateLimiter(5000, 120 * 60 * 1000); private statistics = new InspectionStatistics({ @@ -352,7 +360,7 @@ export class Agent { this.interval.unref(); } - private async updateBlockedLists() { + async updateBlockedLists() { if (!this.token) { return; } @@ -363,11 +371,11 @@ export class Agent { } try { - const { blockedIPAddresses, blockedUserAgents } = await fetchBlockedLists( - this.token - ); + const { blockedIPAddresses, blockedUserAgents, allowedIPAddresses } = + await fetchBlockedLists(this.token); this.serviceConfig.updateBlockedIPAddresses(blockedIPAddresses); this.serviceConfig.updateBlockedUserAgents(blockedUserAgents); + this.serviceConfig.updateOnlyAllowedIPAddresses(allowedIPAddresses); } catch (error: any) { console.error(`Aikido: Failed to update blocked lists: ${error.message}`); } diff --git a/library/agent/ServiceConfig.test.ts b/library/agent/ServiceConfig.test.ts index b2fc4abaf..bdbd5184d 100644 --- a/library/agent/ServiceConfig.test.ts +++ b/library/agent/ServiceConfig.test.ts @@ -2,7 +2,7 @@ import * as t from "tap"; import { ServiceConfig } from "./ServiceConfig"; t.test("it returns false if empty rules", async () => { - const config = new ServiceConfig([], 0, [], [], false, []); + const config = new ServiceConfig([], 0, [], [], false, [], []); t.same(config.getLastUpdatedAt(), 0); t.same(config.isUserBlocked("id"), false); t.same(config.isAllowedIP("1.2.3.4"), false); @@ -54,6 +54,7 @@ t.test("it works", async () => { ["123"], [], false, + [], [] ); @@ -81,25 +82,33 @@ t.test("it works", async () => { }); t.test("it checks if IP is allowed", async () => { - const config = new ServiceConfig([], 0, [], ["1.2.3.4"], false, []); + const config = new ServiceConfig([], 0, [], ["1.2.3.4"], false, [], []); t.same(config.isAllowedIP("1.2.3.4"), true); t.same(config.isAllowedIP("1.2.3.5"), false); }); t.test("ip blocking works", async () => { - const config = new ServiceConfig([], 0, [], [], false, [ - { - source: "geoip", - description: "description", - ips: [ - "1.2.3.4", - "192.168.2.1/24", - "fd00:1234:5678:9abc::1", - "fd00:3234:5678:9abc::1/64", - "5.6.7.8/32", - ], - }, - ]); + const config = new ServiceConfig( + [], + 0, + [], + [], + false, + [ + { + source: "geoip", + description: "description", + ips: [ + "1.2.3.4", + "192.168.2.1/24", + "fd00:1234:5678:9abc::1", + "fd00:3234:5678:9abc::1/64", + "5.6.7.8/32", + ], + }, + ], + [] + ); t.same(config.isIPAddressBlocked("1.2.3.4"), { blocked: true, reason: "description", @@ -132,14 +141,42 @@ t.test("ip blocking works", async () => { }); t.test("it blocks bots", async () => { - const config = new ServiceConfig([], 0, [], [], true, []); + const config = new ServiceConfig([], 0, [], [], true, [], []); config.updateBlockedUserAgents("googlebot|bingbot"); t.same(config.isUserAgentBlocked("googlebot"), { blocked: true }); t.same(config.isUserAgentBlocked("123 bingbot abc"), { blocked: true }); t.same(config.isUserAgentBlocked("bing"), { blocked: false }); + t.same(config.shouldOnlyAllowSomeIPAddresses(), false); + config.updateBlockedUserAgents(""); t.same(config.isUserAgentBlocked("googlebot"), { blocked: false }); }); + +t.test("restricting access to some ips", async () => { + const config = new ServiceConfig( + [], + 0, + [], + [], + true, + [], + [ + { + source: "geoip", + description: "description", + ips: ["1.2.3.4"], + }, + ] + ); + + t.same(config.shouldOnlyAllowSomeIPAddresses(), true); + + t.same(config.isOnlyAllowedIPAddress("1.2.3.4"), true); + t.same(config.isOnlyAllowedIPAddress("4.3.2.1"), false); + + config.updateOnlyAllowedIPAddresses([]); + t.same(config.isOnlyAllowedIPAddress("1.2.3.4"), false); +}); diff --git a/library/agent/ServiceConfig.ts b/library/agent/ServiceConfig.ts index 7d15e2306..21ad93782 100644 --- a/library/agent/ServiceConfig.ts +++ b/library/agent/ServiceConfig.ts @@ -1,7 +1,7 @@ import { IPMatcher } from "../helpers/ip-matcher/IPMatcher"; import { LimitedContext, matchEndpoints } from "../helpers/matchEndpoints"; import { Endpoint } from "./Config"; -import { Blocklist as BlocklistType } from "./api/fetchBlockedLists"; +import { IPList as IPList } from "./api/fetchBlockedLists"; export class ServiceConfig { private blockedUserIds: Map = new Map(); @@ -11,6 +11,7 @@ export class ServiceConfig { private blockedIPAddresses: { blocklist: IPMatcher; description: string }[] = []; private blockedUserAgentRegex: RegExp | undefined; + private onlyAllowedIPAddresses: IPMatcher | undefined; constructor( endpoints: Endpoint[], @@ -18,12 +19,14 @@ export class ServiceConfig { blockedUserIds: string[], allowedIPAddresses: string[], private receivedAnyStats: boolean, - blockedIPAddresses: BlocklistType[] + blockedIPAddresses: IPList[], + onlyAllowedIPAddresses: IPList[] ) { this.setBlockedUserIds(blockedUserIds); this.setAllowedIPAddresses(allowedIPAddresses); this.setEndpoints(endpoints); this.setBlockedIPAddresses(blockedIPAddresses); + this.setOnlyAllowedIPAddresses(onlyAllowedIPAddresses); } private setEndpoints(endpoints: Endpoint[]) { @@ -96,7 +99,7 @@ export class ServiceConfig { return { blocked: false }; } - private setBlockedIPAddresses(blockedIPAddresses: BlocklistType[]) { + private setBlockedIPAddresses(blockedIPAddresses: IPList[]) { this.blockedIPAddresses = []; for (const source of blockedIPAddresses) { @@ -107,7 +110,7 @@ export class ServiceConfig { } } - updateBlockedIPAddresses(blockedIPAddresses: BlocklistType[]) { + updateBlockedIPAddresses(blockedIPAddresses: IPList[]) { this.setBlockedIPAddresses(blockedIPAddresses); } @@ -126,6 +129,32 @@ export class ServiceConfig { return { blocked: false }; } + private setOnlyAllowedIPAddresses(ipAddresses: IPList[]) { + this.onlyAllowedIPAddresses = undefined; + + if (ipAddresses.length === 0) { + return; + } + + const ips = ipAddresses.map((source) => source.ips).flat(); + + this.onlyAllowedIPAddresses = new IPMatcher(ips); + } + + updateOnlyAllowedIPAddresses(ipAddresses: IPList[]) { + this.setOnlyAllowedIPAddresses(ipAddresses); + } + + shouldOnlyAllowSomeIPAddresses() { + return this.onlyAllowedIPAddresses !== undefined; + } + + isOnlyAllowedIPAddress(ip: string) { + return this.onlyAllowedIPAddresses + ? this.onlyAllowedIPAddresses.has(ip) + : false; + } + updateConfig( endpoints: Endpoint[], lastUpdatedAt: number, diff --git a/library/agent/api/fetchBlockedLists.ts b/library/agent/api/fetchBlockedLists.ts index 3e2db2719..91473b5d6 100644 --- a/library/agent/api/fetchBlockedLists.ts +++ b/library/agent/api/fetchBlockedLists.ts @@ -2,14 +2,15 @@ import { fetch } from "../../helpers/fetch"; import { getAPIURL } from "../getAPIURL"; import { Token } from "./Token"; -export type Blocklist = { +export type IPList = { source: string; description: string; ips: string[]; }; export async function fetchBlockedLists(token: Token): Promise<{ - blockedIPAddresses: Blocklist[]; + blockedIPAddresses: IPList[]; + allowedIPAddresses: IPList[]; blockedUserAgents: string; }> { const baseUrl = getAPIURL(); @@ -34,7 +35,8 @@ export async function fetchBlockedLists(token: Token): Promise<{ } const result: { - blockedIPAddresses: Blocklist[]; + blockedIPAddresses: IPList[]; + allowedIPAddresses: IPList[]; blockedUserAgents: string; } = JSON.parse(body); @@ -43,6 +45,10 @@ export async function fetchBlockedLists(token: Token): Promise<{ result && Array.isArray(result.blockedIPAddresses) ? result.blockedIPAddresses : [], + allowedIPAddresses: + result && Array.isArray(result.allowedIPAddresses) + ? result.allowedIPAddresses + : [], // Blocked user agents are stored as a string pattern for usage in a regex (e.g. "Googlebot|Bingbot") blockedUserAgents: result && typeof result.blockedUserAgents === "string" diff --git a/library/ratelimiting/getRateLimitedEndpoint.test.ts b/library/ratelimiting/getRateLimitedEndpoint.test.ts index c5fcec0ce..fd85762a4 100644 --- a/library/ratelimiting/getRateLimitedEndpoint.test.ts +++ b/library/ratelimiting/getRateLimitedEndpoint.test.ts @@ -18,7 +18,10 @@ const context: Context = { t.test("it returns undefined if no endpoints", async () => { t.same( - getRateLimitedEndpoint(context, new ServiceConfig([], 0, [], [], true, [])), + getRateLimitedEndpoint( + context, + new ServiceConfig([], 0, [], [], true, [], []) + ), undefined ); }); @@ -45,6 +48,7 @@ t.test("it returns undefined if no matching endpoints", async () => { [], [], false, + [], [] ) ), @@ -74,6 +78,7 @@ t.test("it returns undefined if matching but not enabled", async () => { [], [], false, + [], [] ) ), @@ -103,6 +108,7 @@ t.test("it returns endpoint if matching and enabled", async () => { [], [], false, + [], [] ) ), @@ -153,6 +159,7 @@ t.test("it returns endpoint with lowest max requests", async () => { [], [], false, + [], [] ) ), @@ -203,6 +210,7 @@ t.test("it returns endpoint with smallest window size", async () => { [], [], false, + [], [] ) ), @@ -253,6 +261,7 @@ t.test("it always returns exact matches first", async () => { [], [], false, + [], [] ) ), diff --git a/library/sources/HTTPServer.test.ts b/library/sources/HTTPServer.test.ts index 743728010..5508ad7c6 100644 --- a/library/sources/HTTPServer.test.ts +++ b/library/sources/HTTPServer.test.ts @@ -8,7 +8,7 @@ import { wrap } from "../helpers/wrap"; import { HTTPServer } from "./HTTPServer"; import { join } from "path"; import { createTestAgent } from "../helpers/createTestAgent"; -import type { Blocklist } from "../agent/api/fetchBlockedLists"; +import type { IPList } from "../agent/api/fetchBlockedLists"; import * as fetchBlockedLists from "../agent/api/fetchBlockedLists"; import { mkdtemp, writeFile, unlink } from "fs/promises"; import { exec } from "child_process"; @@ -52,7 +52,7 @@ agent.start([new HTTPServer()]); wrap(fetchBlockedLists, "fetchBlockedLists", function fetchBlockedLists() { return async function fetchBlockedLists(): Promise<{ - blockedIPAddresses: Blocklist[]; + blockedIPAddresses: IPList[]; blockedUserAgents: string; }> { return { diff --git a/library/sources/Hono.test.ts b/library/sources/Hono.test.ts index 015d1d7bd..6a3a45f6b 100644 --- a/library/sources/Hono.test.ts +++ b/library/sources/Hono.test.ts @@ -12,7 +12,10 @@ import { isLocalhostIP } from "../helpers/isLocalhostIP"; import { createTestAgent } from "../helpers/createTestAgent"; import { addHonoMiddleware } from "../middleware/hono"; import * as fetch from "../helpers/fetch"; +import { setTimeout } from "node:timers/promises"; +import { getInstance } from "../agent/AgentSingleton"; +let shouldReturnAllowedIPAddresses = false; wrap(fetch, "fetch", function mock(original) { return async function mock(this: typeof fetch) { if ( @@ -31,6 +34,15 @@ wrap(fetch, "fetch", function mock(original) { }, ], blockedUserAgents: "hacker|attacker", + allowedIPAddresses: shouldReturnAllowedIPAddresses + ? [ + { + source: "geoip", + description: "geo restrictions", + ips: ["4.3.2.1"], + }, + ] + : [], }), }; } @@ -506,3 +518,30 @@ t.test("invalid json body", opts, async (t) => { t.same(response.status, 400); t.same(await response.text(), "Invalid JSON"); }); + +t.test("test access only allowed for some IP addresses", opts, async (t) => { + // Update the allowed IP addresses + shouldReturnAllowedIPAddresses = true; + await getInstance()!.updateBlockedLists(); + + const { serve } = + require("@hono/node-server") as typeof import("@hono/node-server"); + const server = serve({ + fetch: getApp().fetch, + port: 8768, + }); + + const response = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "1.3.2.4", + }, + }); + t.equal(response.statusCode, 403); + t.equal( + response.body, + "our IP address is not allowed to access this resource. (Your IP: 1.3.2.4)" + ); + + server.close(); +}); diff --git a/library/sources/http-server/checkIfRequestIsBlocked.ts b/library/sources/http-server/checkIfRequestIsBlocked.ts index 4be81c7f8..b399cf1a7 100644 --- a/library/sources/http-server/checkIfRequestIsBlocked.ts +++ b/library/sources/http-server/checkIfRequestIsBlocked.ts @@ -25,6 +25,24 @@ export function checkIfRequestIsBlocked( return false; } + if ( + context.remoteAddress && + agent.getConfig().shouldOnlyAllowSomeIPAddresses() && + !agent.getConfig().isOnlyAllowedIPAddress(context.remoteAddress) + ) { + res.statusCode = 403; + res.setHeader("Content-Type", "text/plain"); + + let message = "Your IP address is not allowed to access this resource."; + if (context.remoteAddress) { + message += ` (Your IP: ${escapeHTML(context.remoteAddress)})`; + } + + res.end(message); + + return true; + } + const result = context.remoteAddress ? agent.getConfig().isIPAddressBlocked(context.remoteAddress) : ({ blocked: false } as const); From 621a6857fdf44864c023b356344913e257321d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 27 Jan 2025 17:29:48 +0100 Subject: [PATCH 02/12] Fix typo --- library/sources/Hono.test.ts | 2 +- library/sources/http-server/checkIfRequestIsBlocked.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/sources/Hono.test.ts b/library/sources/Hono.test.ts index 6a3a45f6b..322448c1e 100644 --- a/library/sources/Hono.test.ts +++ b/library/sources/Hono.test.ts @@ -540,7 +540,7 @@ t.test("test access only allowed for some IP addresses", opts, async (t) => { t.equal(response.statusCode, 403); t.equal( response.body, - "our IP address is not allowed to access this resource. (Your IP: 1.3.2.4)" + "Your IP address is not allowed to access this resource. (Your IP: 1.3.2.4)" ); server.close(); diff --git a/library/sources/http-server/checkIfRequestIsBlocked.ts b/library/sources/http-server/checkIfRequestIsBlocked.ts index b399cf1a7..fe1392d01 100644 --- a/library/sources/http-server/checkIfRequestIsBlocked.ts +++ b/library/sources/http-server/checkIfRequestIsBlocked.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines-per-function */ import type { ServerResponse } from "http"; import { Agent } from "../../agent/Agent"; import { getContext } from "../../agent/Context"; From 2561ff5e5f7ee09d35bfeaa09df1621ca7f00d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 27 Jan 2025 17:58:51 +0100 Subject: [PATCH 03/12] Add e2e tests --- end2end/server/src/handlers/lists.js | 12 ++ end2end/server/src/handlers/updateLists.js | 8 + end2end/server/src/zen/config.js | 27 ++++ end2end/tests/hono-xml-allowlists.test.ts | 144 ++++++++++++++++++ .../http-server/checkIfRequestIsBlocked.ts | 16 +- 5 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 end2end/tests/hono-xml-allowlists.test.ts diff --git a/end2end/server/src/handlers/lists.js b/end2end/server/src/handlers/lists.js index 9334a1f2e..617ac2ff0 100644 --- a/end2end/server/src/handlers/lists.js +++ b/end2end/server/src/handlers/lists.js @@ -1,6 +1,7 @@ const { getBlockedIPAddresses, getBlockedUserAgents, + getAllowedIPAddresses, } = require("../zen/config"); module.exports = function lists(req, res) { @@ -10,6 +11,7 @@ module.exports = function lists(req, res) { const blockedIps = getBlockedIPAddresses(req.app); const blockedUserAgents = getBlockedUserAgents(req.app); + const allowedIps = getAllowedIPAddresses(req.app); res.json({ success: true, @@ -25,5 +27,15 @@ module.exports = function lists(req, res) { ] : [], blockedUserAgents: blockedUserAgents, + allowedIPAddresses: + allowedIps.length > 0 + ? [ + { + source: "geoip", + description: "geo restrictions", + ips: allowedIps, + }, + ] + : [], }); }; diff --git a/end2end/server/src/handlers/updateLists.js b/end2end/server/src/handlers/updateLists.js index e0c7d9080..4b0a02164 100644 --- a/end2end/server/src/handlers/updateLists.js +++ b/end2end/server/src/handlers/updateLists.js @@ -1,6 +1,7 @@ const { updateBlockedIPAddresses, updateBlockedUserAgents, + updateAllowedIPAddresses, } = require("../zen/config"); module.exports = function updateIPLists(req, res) { @@ -38,5 +39,12 @@ module.exports = function updateIPLists(req, res) { updateBlockedUserAgents(req.app, req.body.blockedUserAgents); } + if ( + req.body.allowedIPAddresses && + Array.isArray(req.body.allowedIPAddresses) + ) { + updateAllowedIPAddresses(req.app, req.body.allowedIPAddresses); + } + res.json({ success: true }); }; diff --git a/end2end/server/src/zen/config.js b/end2end/server/src/zen/config.js index 77564fb8c..09cfd8f51 100644 --- a/end2end/server/src/zen/config.js +++ b/end2end/server/src/zen/config.js @@ -39,6 +39,7 @@ function updateAppConfig(app, newConfig) { const blockedIPAddresses = []; const blockedUserAgents = []; +const allowedIPAddresses = []; function updateBlockedIPAddresses(app, ips) { let entry = blockedIPAddresses.find((ip) => ip.serviceId === app.serviceId); @@ -64,6 +65,30 @@ function getBlockedIPAddresses(app) { return { serviceId: app.serviceId, ipAddresses: [] }; } +function updateAllowedIPAddresses(app, ips) { + let entry = allowedIPAddresses.find((ip) => ip.serviceId === app.serviceId); + + if (entry) { + entry.ipAddresses = ips; + } else { + entry = { serviceId: app.serviceId, ipAddresses: ips }; + allowedIPAddresses.push(entry); + } + + // Bump lastUpdatedAt + updateAppConfig(app, {}); +} + +function getAllowedIPAddresses(app) { + const entry = allowedIPAddresses.find((ip) => ip.serviceId === app.serviceId); + + if (entry) { + return entry.ipAddresses; + } + + return { serviceId: app.serviceId, ipAddresses: [] }; +} + function updateBlockedUserAgents(app, uas) { let entry = blockedUserAgents.find((e) => e.serviceId === e.serviceId); @@ -95,4 +120,6 @@ module.exports = { getBlockedIPAddresses, updateBlockedUserAgents, getBlockedUserAgents, + getAllowedIPAddresses, + updateAllowedIPAddresses, }; diff --git a/end2end/tests/hono-xml-allowlists.test.ts b/end2end/tests/hono-xml-allowlists.test.ts new file mode 100644 index 000000000..56a175055 --- /dev/null +++ b/end2end/tests/hono-xml-allowlists.test.ts @@ -0,0 +1,144 @@ +const t = require("tap"); +const { spawn } = require("child_process"); +const { resolve } = require("path"); +const timeout = require("../timeout"); + +const pathToApp = resolve(__dirname, "../../sample-apps/hono-xml", "app.js"); +const testServerUrl = "http://localhost:5874"; + +let token; +t.beforeEach(async () => { + const response = await fetch(`${testServerUrl}/api/runtime/apps`, { + method: "POST", + }); + const body = await response.json(); + token = body.token; + + const config = await fetch(`${testServerUrl}/api/runtime/config`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: token, + }, + body: JSON.stringify({ + allowedIPAddresses: ["5.6.7.8"], + endpoints: [ + { + route: "/admin", + method: "GET", + forceProtectionOff: false, + allowedIPAddresses: [], + rateLimiting: { + enabled: false, + }, + }, + ], + }), + }); + t.same(config.status, 200); + + const lists = await fetch(`${testServerUrl}/api/runtime/firewall/lists`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: token, + }, + body: JSON.stringify({ + allowedIPAddresses: ["4.3.2.1/32", "fe80::1234:5678:abcd:ef12/64"], + blockedIPAddresses: [], + blockedUserAgents: "hacker|attacker|GPTBot", + }), + }); + t.same(lists.status, 200); +}); + +t.test("it blocks non-allowed IP addresses", (t) => { + const server = spawn(`node`, [pathToApp, "4002"], { + env: { + ...process.env, + AIKIDO_DEBUG: "true", + AIKIDO_BLOCK: "true", + AIKIDO_TOKEN: token, + AIKIDO_URL: testServerUrl, + }, + }); + + server.on("close", () => { + t.end(); + }); + + server.on("error", (err) => { + t.fail(err); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + timeout(2000) + .then(async () => { + const resp1 = await fetch("http://127.0.0.1:4002/add", { + method: "POST", + body: "Njuska", + headers: { + "Content-Type": "application/xml", + "X-Forwarded-For": "1.3.2.4", + }, + signal: AbortSignal.timeout(5000), + }); + t.same(resp1.status, 403); + t.same( + await resp1.text(), + "Your IP address is not allowed to access this resource. (Your IP: 1.3.2.4)" + ); + + const resp2 = await fetch("http://127.0.0.1:4002/add", { + method: "POST", + body: "Harry", + headers: { + "Content-Type": "application/xml", + "X-Forwarded-For": "fe80::1234:5678:abcd:ef12", + }, + signal: AbortSignal.timeout(5000), + }); + t.same(resp2.status, 200); + t.same(await resp2.text(), JSON.stringify({ success: true })); + + const resp3 = await fetch("http://127.0.0.1:4002/add", { + method: "POST", + body: "Harry", + headers: { + "Content-Type": "application/xml", + "X-Forwarded-For": "4.3.2.1", + }, + signal: AbortSignal.timeout(5000), + }); + t.same(resp3.status, 200); + t.same(await resp3.text(), JSON.stringify({ success: true })); + + const resp4 = await fetch("http://127.0.0.1:4002/add", { + method: "POST", + body: "Harry2", + headers: { + "Content-Type": "application/xml", + "X-Forwarded-For": "5.6.7.8", + }, + signal: AbortSignal.timeout(5000), + }); + t.same(resp4.status, 200); + t.same(await resp4.text(), JSON.stringify({ success: true })); + }) + .catch((error) => { + t.fail(error); + }) + .finally(() => { + server.kill(); + }); +}); diff --git a/library/sources/http-server/checkIfRequestIsBlocked.ts b/library/sources/http-server/checkIfRequestIsBlocked.ts index 74d75dcb8..0d323eb84 100644 --- a/library/sources/http-server/checkIfRequestIsBlocked.ts +++ b/library/sources/http-server/checkIfRequestIsBlocked.ts @@ -40,6 +40,14 @@ export function checkIfRequestIsBlocked( return true; } + const isAllowedIP = + context.remoteAddress && + agent.getConfig().isAllowedIP(context.remoteAddress); + + if (isAllowedIP) { + return false; + } + if ( context.remoteAddress && agent.getConfig().shouldOnlyAllowSomeIPAddresses() && @@ -58,14 +66,6 @@ export function checkIfRequestIsBlocked( return true; } - const isAllowedIP = - context.remoteAddress && - agent.getConfig().isAllowedIP(context.remoteAddress); - - if (isAllowedIP) { - return false; - } - const result = context.remoteAddress ? agent.getConfig().isIPAddressBlocked(context.remoteAddress) : ({ blocked: false } as const); From 58e69008eee1aa6097da438098c155db856f9837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Fri, 31 Jan 2025 09:37:14 +0100 Subject: [PATCH 04/12] Add comment, fix import --- library/agent/ServiceConfig.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/agent/ServiceConfig.ts b/library/agent/ServiceConfig.ts index 21ad93782..9c493d2b1 100644 --- a/library/agent/ServiceConfig.ts +++ b/library/agent/ServiceConfig.ts @@ -1,7 +1,7 @@ import { IPMatcher } from "../helpers/ip-matcher/IPMatcher"; import { LimitedContext, matchEndpoints } from "../helpers/matchEndpoints"; import { Endpoint } from "./Config"; -import { IPList as IPList } from "./api/fetchBlockedLists"; +import { IPList } from "./api/fetchBlockedLists"; export class ServiceConfig { private blockedUserIds: Map = new Map(); @@ -145,6 +145,9 @@ export class ServiceConfig { this.setOnlyAllowedIPAddresses(ipAddresses); } + /** + * Returns true if only some IP addresses are allowed to access the service, e.g. if a geoip country allowlist is set. + */ shouldOnlyAllowSomeIPAddresses() { return this.onlyAllowedIPAddresses !== undefined; } From 8070e0d3e503552f780f2a616c0cbe6cc077e44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 3 Feb 2025 10:48:04 +0100 Subject: [PATCH 05/12] Rename to onlyAllowedIPAddresses --- end2end/server/src/handlers/lists.js | 6 +++--- end2end/server/src/handlers/updateLists.js | 8 ++++---- end2end/server/src/zen/config.js | 20 ++++++++++++-------- end2end/tests/hono-xml-allowlists.test.ts | 2 +- library/agent/Agent.test.ts | 2 +- library/agent/Agent.ts | 4 ++-- library/agent/ServiceConfig.ts | 7 ++----- library/agent/api/fetchBlockedLists.ts | 10 +++++----- library/sources/Hono.test.ts | 7 +++---- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/end2end/server/src/handlers/lists.js b/end2end/server/src/handlers/lists.js index 617ac2ff0..adaa5ac9e 100644 --- a/end2end/server/src/handlers/lists.js +++ b/end2end/server/src/handlers/lists.js @@ -1,7 +1,7 @@ const { getBlockedIPAddresses, getBlockedUserAgents, - getAllowedIPAddresses, + getOnlyAllowedIPAddresses, } = require("../zen/config"); module.exports = function lists(req, res) { @@ -11,7 +11,7 @@ module.exports = function lists(req, res) { const blockedIps = getBlockedIPAddresses(req.app); const blockedUserAgents = getBlockedUserAgents(req.app); - const allowedIps = getAllowedIPAddresses(req.app); + const allowedIps = getOnlyAllowedIPAddresses(req.app); res.json({ success: true, @@ -27,7 +27,7 @@ module.exports = function lists(req, res) { ] : [], blockedUserAgents: blockedUserAgents, - allowedIPAddresses: + onlyAllowedIPAddresses: allowedIps.length > 0 ? [ { diff --git a/end2end/server/src/handlers/updateLists.js b/end2end/server/src/handlers/updateLists.js index 4b0a02164..7dbdea791 100644 --- a/end2end/server/src/handlers/updateLists.js +++ b/end2end/server/src/handlers/updateLists.js @@ -1,7 +1,7 @@ const { updateBlockedIPAddresses, updateBlockedUserAgents, - updateAllowedIPAddresses, + updateOnlyAllowedIPAddresses, } = require("../zen/config"); module.exports = function updateIPLists(req, res) { @@ -40,10 +40,10 @@ module.exports = function updateIPLists(req, res) { } if ( - req.body.allowedIPAddresses && - Array.isArray(req.body.allowedIPAddresses) + req.body.onlyAllowedIPAddresses && + Array.isArray(req.body.onlyAllowedIPAddresses) ) { - updateAllowedIPAddresses(req.app, req.body.allowedIPAddresses); + updateOnlyAllowedIPAddresses(req.app, req.body.onlyAllowedIPAddresses); } res.json({ success: true }); diff --git a/end2end/server/src/zen/config.js b/end2end/server/src/zen/config.js index 09cfd8f51..9b2768f80 100644 --- a/end2end/server/src/zen/config.js +++ b/end2end/server/src/zen/config.js @@ -39,7 +39,7 @@ function updateAppConfig(app, newConfig) { const blockedIPAddresses = []; const blockedUserAgents = []; -const allowedIPAddresses = []; +const onlyAllowedIPAddresses = []; function updateBlockedIPAddresses(app, ips) { let entry = blockedIPAddresses.find((ip) => ip.serviceId === app.serviceId); @@ -65,22 +65,26 @@ function getBlockedIPAddresses(app) { return { serviceId: app.serviceId, ipAddresses: [] }; } -function updateAllowedIPAddresses(app, ips) { - let entry = allowedIPAddresses.find((ip) => ip.serviceId === app.serviceId); +function updateOnlyAllowedIPAddresses(app, ips) { + let entry = onlyAllowedIPAddresses.find( + (ip) => ip.serviceId === app.serviceId + ); if (entry) { entry.ipAddresses = ips; } else { entry = { serviceId: app.serviceId, ipAddresses: ips }; - allowedIPAddresses.push(entry); + onlyAllowedIPAddresses.push(entry); } // Bump lastUpdatedAt updateAppConfig(app, {}); } -function getAllowedIPAddresses(app) { - const entry = allowedIPAddresses.find((ip) => ip.serviceId === app.serviceId); +function getOnlyAllowedIPAddresses(app) { + const entry = onlyAllowedIPAddresses.find( + (ip) => ip.serviceId === app.serviceId + ); if (entry) { return entry.ipAddresses; @@ -120,6 +124,6 @@ module.exports = { getBlockedIPAddresses, updateBlockedUserAgents, getBlockedUserAgents, - getAllowedIPAddresses, - updateAllowedIPAddresses, + getOnlyAllowedIPAddresses, + updateOnlyAllowedIPAddresses, }; diff --git a/end2end/tests/hono-xml-allowlists.test.ts b/end2end/tests/hono-xml-allowlists.test.ts index 56a175055..3d7fe4e43 100644 --- a/end2end/tests/hono-xml-allowlists.test.ts +++ b/end2end/tests/hono-xml-allowlists.test.ts @@ -44,7 +44,7 @@ t.beforeEach(async () => { Authorization: token, }, body: JSON.stringify({ - allowedIPAddresses: ["4.3.2.1/32", "fe80::1234:5678:abcd:ef12/64"], + onlyAllowedIPAddresses: ["4.3.2.1/32", "fe80::1234:5678:abcd:ef12/64"], blockedIPAddresses: [], blockedUserAgents: "hacker|attacker|GPTBot", }), diff --git a/library/agent/Agent.test.ts b/library/agent/Agent.test.ts index daf5910e8..c8f20b38d 100644 --- a/library/agent/Agent.test.ts +++ b/library/agent/Agent.test.ts @@ -34,7 +34,7 @@ wrap(fetch, "fetch", function mock() { }, ], blockedUserAgents: "AI2Bot|Bytespider", - allowedIPAddresses: shouldOnlyAllowSomeIPAddresses + onlyAllowedIPAddresses: shouldOnlyAllowSomeIPAddresses ? [ { source: "name", diff --git a/library/agent/Agent.ts b/library/agent/Agent.ts index 1aa627b3d..a1633c1e7 100644 --- a/library/agent/Agent.ts +++ b/library/agent/Agent.ts @@ -371,11 +371,11 @@ export class Agent { } try { - const { blockedIPAddresses, blockedUserAgents, allowedIPAddresses } = + const { blockedIPAddresses, blockedUserAgents, onlyAllowedIPAddresses } = await fetchBlockedLists(this.token); this.serviceConfig.updateBlockedIPAddresses(blockedIPAddresses); this.serviceConfig.updateBlockedUserAgents(blockedUserAgents); - this.serviceConfig.updateOnlyAllowedIPAddresses(allowedIPAddresses); + this.serviceConfig.updateOnlyAllowedIPAddresses(onlyAllowedIPAddresses); } catch (error: any) { console.error(`Aikido: Failed to update blocked lists: ${error.message}`); } diff --git a/library/agent/ServiceConfig.ts b/library/agent/ServiceConfig.ts index 9c493d2b1..8e7e367fb 100644 --- a/library/agent/ServiceConfig.ts +++ b/library/agent/ServiceConfig.ts @@ -5,7 +5,7 @@ import { IPList } from "./api/fetchBlockedLists"; export class ServiceConfig { private blockedUserIds: Map = new Map(); - private allowedIPAddresses: Map = new Map(); + private allowedIPAddresses: Set = new Set(); private nonGraphQLEndpoints: Endpoint[] = []; private graphqlFields: Endpoint[] = []; private blockedIPAddresses: { blocklist: IPMatcher; description: string }[] = @@ -64,10 +64,7 @@ export class ServiceConfig { } private setAllowedIPAddresses(allowedIPAddresses: string[]) { - this.allowedIPAddresses = new Map(); - allowedIPAddresses.forEach((ip) => { - this.allowedIPAddresses.set(ip, ip); - }); + this.allowedIPAddresses = new Set(allowedIPAddresses); } isAllowedIP(ip: string) { diff --git a/library/agent/api/fetchBlockedLists.ts b/library/agent/api/fetchBlockedLists.ts index 91473b5d6..6189b0352 100644 --- a/library/agent/api/fetchBlockedLists.ts +++ b/library/agent/api/fetchBlockedLists.ts @@ -10,7 +10,7 @@ export type IPList = { export async function fetchBlockedLists(token: Token): Promise<{ blockedIPAddresses: IPList[]; - allowedIPAddresses: IPList[]; + onlyAllowedIPAddresses: IPList[]; blockedUserAgents: string; }> { const baseUrl = getAPIURL(); @@ -36,7 +36,7 @@ export async function fetchBlockedLists(token: Token): Promise<{ const result: { blockedIPAddresses: IPList[]; - allowedIPAddresses: IPList[]; + onlyAllowedIPAddresses: IPList[]; blockedUserAgents: string; } = JSON.parse(body); @@ -45,9 +45,9 @@ export async function fetchBlockedLists(token: Token): Promise<{ result && Array.isArray(result.blockedIPAddresses) ? result.blockedIPAddresses : [], - allowedIPAddresses: - result && Array.isArray(result.allowedIPAddresses) - ? result.allowedIPAddresses + onlyAllowedIPAddresses: + result && Array.isArray(result.onlyAllowedIPAddresses) + ? result.onlyAllowedIPAddresses : [], // Blocked user agents are stored as a string pattern for usage in a regex (e.g. "Googlebot|Bingbot") blockedUserAgents: diff --git a/library/sources/Hono.test.ts b/library/sources/Hono.test.ts index 322448c1e..a42b394b6 100644 --- a/library/sources/Hono.test.ts +++ b/library/sources/Hono.test.ts @@ -12,10 +12,9 @@ import { isLocalhostIP } from "../helpers/isLocalhostIP"; import { createTestAgent } from "../helpers/createTestAgent"; import { addHonoMiddleware } from "../middleware/hono"; import * as fetch from "../helpers/fetch"; -import { setTimeout } from "node:timers/promises"; import { getInstance } from "../agent/AgentSingleton"; -let shouldReturnAllowedIPAddresses = false; +let shouldReturnOnlyAllowedIPAddresses = false; wrap(fetch, "fetch", function mock(original) { return async function mock(this: typeof fetch) { if ( @@ -34,7 +33,7 @@ wrap(fetch, "fetch", function mock(original) { }, ], blockedUserAgents: "hacker|attacker", - allowedIPAddresses: shouldReturnAllowedIPAddresses + onlyAllowedIPAddresses: shouldReturnOnlyAllowedIPAddresses ? [ { source: "geoip", @@ -521,7 +520,7 @@ t.test("invalid json body", opts, async (t) => { t.test("test access only allowed for some IP addresses", opts, async (t) => { // Update the allowed IP addresses - shouldReturnAllowedIPAddresses = true; + shouldReturnOnlyAllowedIPAddresses = true; await getInstance()!.updateBlockedLists(); const { serve } = From d54ad5a63885e97ebdb1b9eea69498a7f9923048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 3 Feb 2025 13:46:49 +0100 Subject: [PATCH 06/12] Do not block private ip addresses --- library/sources/Hono.test.ts | 16 ++++++++++++++++ .../http-server/checkIfRequestIsBlocked.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/library/sources/Hono.test.ts b/library/sources/Hono.test.ts index a42b394b6..2cea58c75 100644 --- a/library/sources/Hono.test.ts +++ b/library/sources/Hono.test.ts @@ -542,5 +542,21 @@ t.test("test access only allowed for some IP addresses", opts, async (t) => { "Your IP address is not allowed to access this resource. (Your IP: 1.3.2.4)" ); + const response2 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "4.3.2.1", + }, + }); + t.equal(response2.statusCode, 200); + + const response3 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "127.0.0.1", + }, + }); + t.equal(response3.statusCode, 200); + server.close(); }); diff --git a/library/sources/http-server/checkIfRequestIsBlocked.ts b/library/sources/http-server/checkIfRequestIsBlocked.ts index 0d323eb84..8b4f32fb0 100644 --- a/library/sources/http-server/checkIfRequestIsBlocked.ts +++ b/library/sources/http-server/checkIfRequestIsBlocked.ts @@ -4,6 +4,7 @@ import { Agent } from "../../agent/Agent"; import { getContext } from "../../agent/Context"; import { escapeHTML } from "../../helpers/escapeHTML"; import { ipAllowedToAccessRoute } from "./ipAllowedToAccessRoute"; +import { isPrivateIP } from "../../vulnerabilities/ssrf/isPrivateIP"; /** * Inspects the IP address of the request: @@ -51,6 +52,7 @@ export function checkIfRequestIsBlocked( if ( context.remoteAddress && agent.getConfig().shouldOnlyAllowSomeIPAddresses() && + !isPrivateIP(context.remoteAddress) && !agent.getConfig().isOnlyAllowedIPAddress(context.remoteAddress) ) { res.statusCode = 403; From 61fd0843ad1dd1aa1641a12c84ef5f8a0d325e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 3 Feb 2025 13:50:36 +0100 Subject: [PATCH 07/12] Add test --- library/sources/Hono.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/sources/Hono.test.ts b/library/sources/Hono.test.ts index 2cea58c75..f52c67bac 100644 --- a/library/sources/Hono.test.ts +++ b/library/sources/Hono.test.ts @@ -550,6 +550,7 @@ t.test("test access only allowed for some IP addresses", opts, async (t) => { }); t.equal(response2.statusCode, 200); + // Always allow localhost const response3 = await fetch.fetch({ url: new URL("http://127.0.0.1:8768/"), headers: { @@ -558,5 +559,14 @@ t.test("test access only allowed for some IP addresses", opts, async (t) => { }); t.equal(response3.statusCode, 200); + // Allow private IP ranges + const response4 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "10.0.2.4", + }, + }); + t.equal(response4.statusCode, 200); + server.close(); }); From fd2364c86af8c5d514c912e72e789b736f9d0f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Mon, 3 Feb 2025 14:22:26 +0100 Subject: [PATCH 08/12] Split test file --- library/agent/Agent.ts | 2 +- library/agent/api-discovery/getApiInfo.ts | 2 +- .../sources/Hono.onlyAllowIPAddresses.test.ts | 128 ++++++++++++++++++ library/sources/Hono.test.ts | 65 +-------- 4 files changed, 131 insertions(+), 66 deletions(-) create mode 100644 library/sources/Hono.onlyAllowIPAddresses.test.ts diff --git a/library/agent/Agent.ts b/library/agent/Agent.ts index a1633c1e7..2bda9f558 100644 --- a/library/agent/Agent.ts +++ b/library/agent/Agent.ts @@ -360,7 +360,7 @@ export class Agent { this.interval.unref(); } - async updateBlockedLists() { + private async updateBlockedLists() { if (!this.token) { return; } diff --git a/library/agent/api-discovery/getApiInfo.ts b/library/agent/api-discovery/getApiInfo.ts index bc1d76d1f..202a13e8f 100644 --- a/library/agent/api-discovery/getApiInfo.ts +++ b/library/agent/api-discovery/getApiInfo.ts @@ -9,7 +9,7 @@ export type APISpec = { auth?: APIAuthType[]; }; -export type APIBodyInfo = { +type APIBodyInfo = { type: BodyDataType; schema: DataSchema; }; diff --git a/library/sources/Hono.onlyAllowIPAddresses.test.ts b/library/sources/Hono.onlyAllowIPAddresses.test.ts new file mode 100644 index 000000000..6d0ea6d3d --- /dev/null +++ b/library/sources/Hono.onlyAllowIPAddresses.test.ts @@ -0,0 +1,128 @@ +/* eslint-disable prefer-rest-params */ +import * as t from "tap"; +import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting"; +import { Token } from "../agent/api/Token"; +import { wrap } from "../helpers/wrap"; +import { Hono as HonoInternal } from "./Hono"; +import { HTTPServer } from "./HTTPServer"; +import { getMajorNodeVersion } from "../helpers/getNodeVersion"; +import { createTestAgent } from "../helpers/createTestAgent"; +import * as fetch from "../helpers/fetch"; + +wrap(fetch, "fetch", function mock(original) { + return async function mock(this: typeof fetch) { + if ( + arguments.length > 0 && + arguments[0] && + arguments[0].url.toString().includes("firewall") + ) { + return { + statusCode: 200, + body: JSON.stringify({ + blockedIPAddresses: [ + { + source: "geoip", + description: "geo restrictions", + ips: ["1.3.2.0/24", "fe80::1234:5678:abcd:ef12/64"], + }, + ], + blockedUserAgents: "hacker|attacker", + onlyAllowedIPAddresses: [ + { + source: "geoip", + description: "geo restrictions", + ips: ["4.3.2.1"], + }, + ], + }), + }; + } + + return await original.apply(this, arguments); + }; +}); + +const agent = createTestAgent({ + token: new Token("123"), + api: new ReportingAPIForTesting({ + success: true, + endpoints: [ + { + method: "GET", + route: "/rate-limited", + forceProtectionOff: false, + rateLimiting: { + windowSizeInMS: 2000, + maxRequests: 2, + enabled: true, + }, + }, + ], + blockedUserIds: ["567"], + configUpdatedAt: 0, + heartbeatIntervalInMS: 10 * 60 * 1000, + allowedIPAddresses: ["4.3.2.1"], + }), +}); +agent.start([new HonoInternal(), new HTTPServer()]); +const opts = { + skip: + getMajorNodeVersion() < 18 ? "Hono does not support Node.js < 18" : false, +}; + +t.test("test access only allowed for some IP addresses", opts, async (t) => { + const { Hono } = require("hono") as typeof import("hono"); + const { serve } = + require("@hono/node-server") as typeof import("@hono/node-server"); + + const app = new Hono(); + + app.get("/", (c) => { + return c.text("Hello, world!"); + }); + + const server = serve({ + fetch: app.fetch, + port: 8768, + }); + + const response = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "1.3.2.4", + }, + }); + t.equal(response.statusCode, 403); + t.equal( + response.body, + "Your IP address is not allowed to access this resource. (Your IP: 1.3.2.4)" + ); + + const response2 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "4.3.2.1", + }, + }); + t.equal(response2.statusCode, 200); + + // Always allow localhost + const response3 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "127.0.0.1", + }, + }); + t.equal(response3.statusCode, 200); + + // Allow private IP ranges + const response4 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "10.0.2.4", + }, + }); + t.equal(response4.statusCode, 200); + + server.close(); +}); diff --git a/library/sources/Hono.test.ts b/library/sources/Hono.test.ts index f52c67bac..41cf7776a 100644 --- a/library/sources/Hono.test.ts +++ b/library/sources/Hono.test.ts @@ -12,9 +12,7 @@ import { isLocalhostIP } from "../helpers/isLocalhostIP"; import { createTestAgent } from "../helpers/createTestAgent"; import { addHonoMiddleware } from "../middleware/hono"; import * as fetch from "../helpers/fetch"; -import { getInstance } from "../agent/AgentSingleton"; -let shouldReturnOnlyAllowedIPAddresses = false; wrap(fetch, "fetch", function mock(original) { return async function mock(this: typeof fetch) { if ( @@ -33,15 +31,7 @@ wrap(fetch, "fetch", function mock(original) { }, ], blockedUserAgents: "hacker|attacker", - onlyAllowedIPAddresses: shouldReturnOnlyAllowedIPAddresses - ? [ - { - source: "geoip", - description: "geo restrictions", - ips: ["4.3.2.1"], - }, - ] - : [], + onlyAllowedIPAddresses: [], }), }; } @@ -517,56 +507,3 @@ t.test("invalid json body", opts, async (t) => { t.same(response.status, 400); t.same(await response.text(), "Invalid JSON"); }); - -t.test("test access only allowed for some IP addresses", opts, async (t) => { - // Update the allowed IP addresses - shouldReturnOnlyAllowedIPAddresses = true; - await getInstance()!.updateBlockedLists(); - - const { serve } = - require("@hono/node-server") as typeof import("@hono/node-server"); - const server = serve({ - fetch: getApp().fetch, - port: 8768, - }); - - const response = await fetch.fetch({ - url: new URL("http://127.0.0.1:8768/"), - headers: { - "X-Forwarded-For": "1.3.2.4", - }, - }); - t.equal(response.statusCode, 403); - t.equal( - response.body, - "Your IP address is not allowed to access this resource. (Your IP: 1.3.2.4)" - ); - - const response2 = await fetch.fetch({ - url: new URL("http://127.0.0.1:8768/"), - headers: { - "X-Forwarded-For": "4.3.2.1", - }, - }); - t.equal(response2.statusCode, 200); - - // Always allow localhost - const response3 = await fetch.fetch({ - url: new URL("http://127.0.0.1:8768/"), - headers: { - "X-Forwarded-For": "127.0.0.1", - }, - }); - t.equal(response3.statusCode, 200); - - // Allow private IP ranges - const response4 = await fetch.fetch({ - url: new URL("http://127.0.0.1:8768/"), - headers: { - "X-Forwarded-For": "10.0.2.4", - }, - }); - t.equal(response4.statusCode, 200); - - server.close(); -}); From 0c11c081354e38124c9b91ff6b90d0801b44d9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Tue, 11 Feb 2025 09:15:23 +0100 Subject: [PATCH 09/12] Do not merge allowed ip addresses --- library/agent/Agent.test.ts | 4 +-- library/agent/ServiceConfig.test.ts | 29 +++++++++++++-- library/agent/ServiceConfig.ts | 36 +++++++++++-------- .../http-server/checkIfRequestIsBlocked.ts | 2 +- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/library/agent/Agent.test.ts b/library/agent/Agent.test.ts index c8f20b38d..0d4a660b7 100644 --- a/library/agent/Agent.test.ts +++ b/library/agent/Agent.test.ts @@ -1145,6 +1145,6 @@ t.test("it only allows some IP addresses", async () => { }); t.same(agent.getConfig().shouldOnlyAllowSomeIPAddresses(), true); - t.same(agent.getConfig().isOnlyAllowedIPAddress("1.2.3.4"), false); - t.same(agent.getConfig().isOnlyAllowedIPAddress("4.3.2.1"), true); + t.same(agent.getConfig().isOnlyAllowedIPAddress("1.2.3.4").allowed, false); + t.same(agent.getConfig().isOnlyAllowedIPAddress("4.3.2.1").allowed, true); }); diff --git a/library/agent/ServiceConfig.test.ts b/library/agent/ServiceConfig.test.ts index bdbd5184d..e63125992 100644 --- a/library/agent/ServiceConfig.test.ts +++ b/library/agent/ServiceConfig.test.ts @@ -174,9 +174,32 @@ t.test("restricting access to some ips", async () => { t.same(config.shouldOnlyAllowSomeIPAddresses(), true); - t.same(config.isOnlyAllowedIPAddress("1.2.3.4"), true); - t.same(config.isOnlyAllowedIPAddress("4.3.2.1"), false); + t.same(config.isOnlyAllowedIPAddress("1.2.3.4").allowed, true); + t.same(config.isOnlyAllowedIPAddress("4.3.2.1").allowed, false); config.updateOnlyAllowedIPAddresses([]); - t.same(config.isOnlyAllowedIPAddress("1.2.3.4"), false); + t.same(config.isOnlyAllowedIPAddress("1.2.3.4").allowed, false); +}); + +t.test("only allow some ips: empty list", async () => { + const config = new ServiceConfig( + [], + 0, + [], + [], + true, + [], + [ + { + source: "geoip", + description: "description", + ips: [], + }, + ] + ); + + t.same(config.shouldOnlyAllowSomeIPAddresses(), false); + + t.same(config.isOnlyAllowedIPAddress("1.2.3.4").allowed, false); + t.same(config.isOnlyAllowedIPAddress("4.3.2.1").allowed, false); }); diff --git a/library/agent/ServiceConfig.ts b/library/agent/ServiceConfig.ts index 8e7e367fb..578a35e51 100644 --- a/library/agent/ServiceConfig.ts +++ b/library/agent/ServiceConfig.ts @@ -11,7 +11,10 @@ export class ServiceConfig { private blockedIPAddresses: { blocklist: IPMatcher; description: string }[] = []; private blockedUserAgentRegex: RegExp | undefined; - private onlyAllowedIPAddresses: IPMatcher | undefined; + private onlyAllowedIPAddresses: { + allowlist: IPMatcher; + description: string; + }[] = []; constructor( endpoints: Endpoint[], @@ -127,15 +130,18 @@ export class ServiceConfig { } private setOnlyAllowedIPAddresses(ipAddresses: IPList[]) { - this.onlyAllowedIPAddresses = undefined; - - if (ipAddresses.length === 0) { - return; + this.onlyAllowedIPAddresses = []; + + for (const source of ipAddresses) { + // Skip empty allowlists + if (source.ips.length === 0) { + continue; + } + this.onlyAllowedIPAddresses.push({ + allowlist: new IPMatcher(source.ips), + description: source.description, + }); } - - const ips = ipAddresses.map((source) => source.ips).flat(); - - this.onlyAllowedIPAddresses = new IPMatcher(ips); } updateOnlyAllowedIPAddresses(ipAddresses: IPList[]) { @@ -146,13 +152,15 @@ export class ServiceConfig { * Returns true if only some IP addresses are allowed to access the service, e.g. if a geoip country allowlist is set. */ shouldOnlyAllowSomeIPAddresses() { - return this.onlyAllowedIPAddresses !== undefined; + return this.onlyAllowedIPAddresses.length > 0; } - isOnlyAllowedIPAddress(ip: string) { - return this.onlyAllowedIPAddresses - ? this.onlyAllowedIPAddresses.has(ip) - : false; + isOnlyAllowedIPAddress(ip: string): { allowed: boolean } { + const allowlist = this.onlyAllowedIPAddresses.find((list) => + list.allowlist.has(ip) + ); + + return { allowed: !!allowlist }; } updateConfig( diff --git a/library/sources/http-server/checkIfRequestIsBlocked.ts b/library/sources/http-server/checkIfRequestIsBlocked.ts index 8b4f32fb0..493086d9f 100644 --- a/library/sources/http-server/checkIfRequestIsBlocked.ts +++ b/library/sources/http-server/checkIfRequestIsBlocked.ts @@ -53,7 +53,7 @@ export function checkIfRequestIsBlocked( context.remoteAddress && agent.getConfig().shouldOnlyAllowSomeIPAddresses() && !isPrivateIP(context.remoteAddress) && - !agent.getConfig().isOnlyAllowedIPAddress(context.remoteAddress) + !agent.getConfig().isOnlyAllowedIPAddress(context.remoteAddress).allowed ) { res.statusCode = 403; res.setHeader("Content-Type", "text/plain"); From e353e4e3849a6621d459095f6c02ecca1953259e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Tue, 11 Feb 2025 12:29:08 +0100 Subject: [PATCH 10/12] Add comments --- library/agent/ServiceConfig.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/agent/ServiceConfig.ts b/library/agent/ServiceConfig.ts index 45661598f..a869bef0e 100644 --- a/library/agent/ServiceConfig.ts +++ b/library/agent/ServiceConfig.ts @@ -5,12 +5,15 @@ import { IPList } from "./api/fetchBlockedLists"; export class ServiceConfig { private blockedUserIds: Map = new Map(); + // IP addresses that are allowed to bypass rate limiting, attack blocking, etc. private bypassedIPAddresses: Set = new Set(); private nonGraphQLEndpoints: Endpoint[] = []; private graphqlFields: Endpoint[] = []; private blockedIPAddresses: { blocklist: IPMatcher; description: string }[] = []; private blockedUserAgentRegex: RegExp | undefined; + // If not empty, only ips in this list are allowed to access the service + // e.g. for country allowlists private onlyAllowedIPAddresses: { allowlist: IPMatcher; description: string; From 4cbbb15c0b7e71a247481afedce736b57f635d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Tue, 11 Feb 2025 13:22:58 +0100 Subject: [PATCH 11/12] Rename to allowedIPAddresses --- end2end/server/src/handlers/lists.js | 6 ++-- end2end/server/src/handlers/updateLists.js | 8 ++--- end2end/server/src/zen/config.js | 20 +++++------ end2end/tests/hono-xml-allowlists.test.ts | 2 +- library/agent/Agent.test.ts | 15 +++++--- library/agent/Agent.ts | 4 +-- library/agent/ServiceConfig.test.ts | 21 +++++------ library/agent/ServiceConfig.ts | 35 ++++++++++--------- library/agent/api/fetchBlockedLists.ts | 10 +++--- .../sources/Hono.onlyAllowIPAddresses.test.ts | 2 +- library/sources/Hono.test.ts | 2 +- .../http-server/checkIfRequestIsBlocked.ts | 5 +-- 12 files changed, 64 insertions(+), 66 deletions(-) diff --git a/end2end/server/src/handlers/lists.js b/end2end/server/src/handlers/lists.js index adaa5ac9e..617ac2ff0 100644 --- a/end2end/server/src/handlers/lists.js +++ b/end2end/server/src/handlers/lists.js @@ -1,7 +1,7 @@ const { getBlockedIPAddresses, getBlockedUserAgents, - getOnlyAllowedIPAddresses, + getAllowedIPAddresses, } = require("../zen/config"); module.exports = function lists(req, res) { @@ -11,7 +11,7 @@ module.exports = function lists(req, res) { const blockedIps = getBlockedIPAddresses(req.app); const blockedUserAgents = getBlockedUserAgents(req.app); - const allowedIps = getOnlyAllowedIPAddresses(req.app); + const allowedIps = getAllowedIPAddresses(req.app); res.json({ success: true, @@ -27,7 +27,7 @@ module.exports = function lists(req, res) { ] : [], blockedUserAgents: blockedUserAgents, - onlyAllowedIPAddresses: + allowedIPAddresses: allowedIps.length > 0 ? [ { diff --git a/end2end/server/src/handlers/updateLists.js b/end2end/server/src/handlers/updateLists.js index 7dbdea791..4b0a02164 100644 --- a/end2end/server/src/handlers/updateLists.js +++ b/end2end/server/src/handlers/updateLists.js @@ -1,7 +1,7 @@ const { updateBlockedIPAddresses, updateBlockedUserAgents, - updateOnlyAllowedIPAddresses, + updateAllowedIPAddresses, } = require("../zen/config"); module.exports = function updateIPLists(req, res) { @@ -40,10 +40,10 @@ module.exports = function updateIPLists(req, res) { } if ( - req.body.onlyAllowedIPAddresses && - Array.isArray(req.body.onlyAllowedIPAddresses) + req.body.allowedIPAddresses && + Array.isArray(req.body.allowedIPAddresses) ) { - updateOnlyAllowedIPAddresses(req.app, req.body.onlyAllowedIPAddresses); + updateAllowedIPAddresses(req.app, req.body.allowedIPAddresses); } res.json({ success: true }); diff --git a/end2end/server/src/zen/config.js b/end2end/server/src/zen/config.js index 25ee64104..87797b8e3 100644 --- a/end2end/server/src/zen/config.js +++ b/end2end/server/src/zen/config.js @@ -39,7 +39,7 @@ function updateAppConfig(app, newConfig) { const blockedIPAddresses = []; const blockedUserAgents = []; -const onlyAllowedIPAddresses = []; +const allowedIPAddresses = []; function updateBlockedIPAddresses(app, ips) { let entry = blockedIPAddresses.find((ip) => ip.serviceId === app.serviceId); @@ -65,26 +65,22 @@ function getBlockedIPAddresses(app) { return { serviceId: app.serviceId, ipAddresses: [] }; } -function updateOnlyAllowedIPAddresses(app, ips) { - let entry = onlyAllowedIPAddresses.find( - (ip) => ip.serviceId === app.serviceId - ); +function updateAllowedIPAddresses(app, ips) { + let entry = allowedIPAddresses.find((ip) => ip.serviceId === app.serviceId); if (entry) { entry.ipAddresses = ips; } else { entry = { serviceId: app.serviceId, ipAddresses: ips }; - onlyAllowedIPAddresses.push(entry); + allowedIPAddresses.push(entry); } // Bump lastUpdatedAt updateAppConfig(app, {}); } -function getOnlyAllowedIPAddresses(app) { - const entry = onlyAllowedIPAddresses.find( - (ip) => ip.serviceId === app.serviceId - ); +function getAllowedIPAddresses(app) { + const entry = allowedIPAddresses.find((ip) => ip.serviceId === app.serviceId); if (entry) { return entry.ipAddresses; @@ -124,6 +120,6 @@ module.exports = { getBlockedIPAddresses, updateBlockedUserAgents, getBlockedUserAgents, - getOnlyAllowedIPAddresses, - updateOnlyAllowedIPAddresses, + getAllowedIPAddresses, + updateAllowedIPAddresses, }; diff --git a/end2end/tests/hono-xml-allowlists.test.ts b/end2end/tests/hono-xml-allowlists.test.ts index 3d7fe4e43..56a175055 100644 --- a/end2end/tests/hono-xml-allowlists.test.ts +++ b/end2end/tests/hono-xml-allowlists.test.ts @@ -44,7 +44,7 @@ t.beforeEach(async () => { Authorization: token, }, body: JSON.stringify({ - onlyAllowedIPAddresses: ["4.3.2.1/32", "fe80::1234:5678:abcd:ef12/64"], + allowedIPAddresses: ["4.3.2.1/32", "fe80::1234:5678:abcd:ef12/64"], blockedIPAddresses: [], blockedUserAgents: "hacker|attacker|GPTBot", }), diff --git a/library/agent/Agent.test.ts b/library/agent/Agent.test.ts index 0d4a660b7..6f1898db6 100644 --- a/library/agent/Agent.test.ts +++ b/library/agent/Agent.test.ts @@ -34,7 +34,7 @@ wrap(fetch, "fetch", function mock() { }, ], blockedUserAgents: "AI2Bot|Bytespider", - onlyAllowedIPAddresses: shouldOnlyAllowSomeIPAddresses + allowedIPAddresses: shouldOnlyAllowSomeIPAddresses ? [ { source: "name", @@ -1110,7 +1110,9 @@ t.test("it does not fetch blocked IPs if serverless", async () => { blocked: false, }); - t.same(agent.getConfig().shouldOnlyAllowSomeIPAddresses(), false); + t.same(agent.getConfig().isAllowedIPAddress("1.3.2.4"), { + allowed: true, + }); t.same( agent @@ -1144,7 +1146,10 @@ t.test("it only allows some IP addresses", async () => { reason: "Description", }); - t.same(agent.getConfig().shouldOnlyAllowSomeIPAddresses(), true); - t.same(agent.getConfig().isOnlyAllowedIPAddress("1.2.3.4").allowed, false); - t.same(agent.getConfig().isOnlyAllowedIPAddress("4.3.2.1").allowed, true); + t.same(agent.getConfig().isAllowedIPAddress("1.2.3.4"), { + allowed: false, + }); + t.same(agent.getConfig().isAllowedIPAddress("4.3.2.1"), { + allowed: true, + }); }); diff --git a/library/agent/Agent.ts b/library/agent/Agent.ts index 40f21362c..c3a596331 100644 --- a/library/agent/Agent.ts +++ b/library/agent/Agent.ts @@ -371,11 +371,11 @@ export class Agent { } try { - const { blockedIPAddresses, blockedUserAgents, onlyAllowedIPAddresses } = + const { blockedIPAddresses, blockedUserAgents, allowedIPAddresses } = await fetchBlockedLists(this.token); this.serviceConfig.updateBlockedIPAddresses(blockedIPAddresses); this.serviceConfig.updateBlockedUserAgents(blockedUserAgents); - this.serviceConfig.updateOnlyAllowedIPAddresses(onlyAllowedIPAddresses); + this.serviceConfig.updateAllowedIPAddresses(allowedIPAddresses); } catch (error: any) { console.error(`Aikido: Failed to update blocked lists: ${error.message}`); } diff --git a/library/agent/ServiceConfig.test.ts b/library/agent/ServiceConfig.test.ts index 69006807a..cf8e192f8 100644 --- a/library/agent/ServiceConfig.test.ts +++ b/library/agent/ServiceConfig.test.ts @@ -148,8 +148,6 @@ t.test("it blocks bots", async () => { t.same(config.isUserAgentBlocked("123 bingbot abc"), { blocked: true }); t.same(config.isUserAgentBlocked("bing"), { blocked: false }); - t.same(config.shouldOnlyAllowSomeIPAddresses(), false); - config.updateBlockedUserAgents(""); t.same(config.isUserAgentBlocked("googlebot"), { blocked: false }); @@ -172,13 +170,14 @@ t.test("restricting access to some ips", async () => { ] ); - t.same(config.shouldOnlyAllowSomeIPAddresses(), true); - - t.same(config.isOnlyAllowedIPAddress("1.2.3.4").allowed, true); - t.same(config.isOnlyAllowedIPAddress("4.3.2.1").allowed, false); + t.same(config.isAllowedIPAddress("1.2.3.4").allowed, true); + t.same(config.isAllowedIPAddress("4.3.2.1").allowed, false); + t.same(config.isAllowedIPAddress("127.0.0.1").allowed, true); // Always allow private ips - config.updateOnlyAllowedIPAddresses([]); - t.same(config.isOnlyAllowedIPAddress("1.2.3.4").allowed, false); + config.updateAllowedIPAddresses([]); + t.same(config.isAllowedIPAddress("1.2.3.4").allowed, true); + t.same(config.isAllowedIPAddress("127.0.0.1").allowed, true); + t.same(config.isAllowedIPAddress("4.3.2.1").allowed, true); }); t.test("only allow some ips: empty list", async () => { @@ -198,8 +197,6 @@ t.test("only allow some ips: empty list", async () => { ] ); - t.same(config.shouldOnlyAllowSomeIPAddresses(), false); - - t.same(config.isOnlyAllowedIPAddress("1.2.3.4").allowed, false); - t.same(config.isOnlyAllowedIPAddress("4.3.2.1").allowed, false); + t.same(config.isAllowedIPAddress("1.2.3.4").allowed, true); + t.same(config.isAllowedIPAddress("4.3.2.1").allowed, true); }); diff --git a/library/agent/ServiceConfig.ts b/library/agent/ServiceConfig.ts index a869bef0e..b770fa0bd 100644 --- a/library/agent/ServiceConfig.ts +++ b/library/agent/ServiceConfig.ts @@ -1,5 +1,6 @@ import { IPMatcher } from "../helpers/ip-matcher/IPMatcher"; import { LimitedContext, matchEndpoints } from "../helpers/matchEndpoints"; +import { isPrivateIP } from "../vulnerabilities/ssrf/isPrivateIP"; import { Endpoint } from "./Config"; import { IPList } from "./api/fetchBlockedLists"; @@ -14,7 +15,7 @@ export class ServiceConfig { private blockedUserAgentRegex: RegExp | undefined; // If not empty, only ips in this list are allowed to access the service // e.g. for country allowlists - private onlyAllowedIPAddresses: { + private allowedIPAddresses: { allowlist: IPMatcher; description: string; }[] = []; @@ -26,13 +27,13 @@ export class ServiceConfig { bypassedIPAddresses: string[], private receivedAnyStats: boolean, blockedIPAddresses: IPList[], - onlyAllowedIPAddresses: IPList[] + allowedIPAddresses: IPList[] ) { this.setBlockedUserIds(blockedUserIds); this.setBypassedIPAddresses(bypassedIPAddresses); this.setEndpoints(endpoints); this.setBlockedIPAddresses(blockedIPAddresses); - this.setOnlyAllowedIPAddresses(onlyAllowedIPAddresses); + this.setAllowedIPAddresses(allowedIPAddresses); } private setEndpoints(endpoints: Endpoint[]) { @@ -135,34 +136,36 @@ export class ServiceConfig { return { blocked: false }; } - private setOnlyAllowedIPAddresses(ipAddresses: IPList[]) { - this.onlyAllowedIPAddresses = []; + private setAllowedIPAddresses(ipAddresses: IPList[]) { + this.allowedIPAddresses = []; for (const source of ipAddresses) { // Skip empty allowlists if (source.ips.length === 0) { continue; } - this.onlyAllowedIPAddresses.push({ + this.allowedIPAddresses.push({ allowlist: new IPMatcher(source.ips), description: source.description, }); } } - updateOnlyAllowedIPAddresses(ipAddresses: IPList[]) { - this.setOnlyAllowedIPAddresses(ipAddresses); + updateAllowedIPAddresses(ipAddresses: IPList[]) { + this.setAllowedIPAddresses(ipAddresses); } - /** - * Returns true if only some IP addresses are allowed to access the service, e.g. if a geoip country allowlist is set. - */ - shouldOnlyAllowSomeIPAddresses() { - return this.onlyAllowedIPAddresses.length > 0; - } + isAllowedIPAddress(ip: string): { allowed: boolean } { + if (this.allowedIPAddresses.length < 1) { + return { allowed: true }; + } + + // Always allow access from local IP addresses + if (isPrivateIP(ip)) { + return { allowed: true }; + } - isOnlyAllowedIPAddress(ip: string): { allowed: boolean } { - const allowlist = this.onlyAllowedIPAddresses.find((list) => + const allowlist = this.allowedIPAddresses.find((list) => list.allowlist.has(ip) ); diff --git a/library/agent/api/fetchBlockedLists.ts b/library/agent/api/fetchBlockedLists.ts index 6189b0352..91473b5d6 100644 --- a/library/agent/api/fetchBlockedLists.ts +++ b/library/agent/api/fetchBlockedLists.ts @@ -10,7 +10,7 @@ export type IPList = { export async function fetchBlockedLists(token: Token): Promise<{ blockedIPAddresses: IPList[]; - onlyAllowedIPAddresses: IPList[]; + allowedIPAddresses: IPList[]; blockedUserAgents: string; }> { const baseUrl = getAPIURL(); @@ -36,7 +36,7 @@ export async function fetchBlockedLists(token: Token): Promise<{ const result: { blockedIPAddresses: IPList[]; - onlyAllowedIPAddresses: IPList[]; + allowedIPAddresses: IPList[]; blockedUserAgents: string; } = JSON.parse(body); @@ -45,9 +45,9 @@ export async function fetchBlockedLists(token: Token): Promise<{ result && Array.isArray(result.blockedIPAddresses) ? result.blockedIPAddresses : [], - onlyAllowedIPAddresses: - result && Array.isArray(result.onlyAllowedIPAddresses) - ? result.onlyAllowedIPAddresses + allowedIPAddresses: + result && Array.isArray(result.allowedIPAddresses) + ? result.allowedIPAddresses : [], // Blocked user agents are stored as a string pattern for usage in a regex (e.g. "Googlebot|Bingbot") blockedUserAgents: diff --git a/library/sources/Hono.onlyAllowIPAddresses.test.ts b/library/sources/Hono.onlyAllowIPAddresses.test.ts index 6d0ea6d3d..e6e5fd0d5 100644 --- a/library/sources/Hono.onlyAllowIPAddresses.test.ts +++ b/library/sources/Hono.onlyAllowIPAddresses.test.ts @@ -27,7 +27,7 @@ wrap(fetch, "fetch", function mock(original) { }, ], blockedUserAgents: "hacker|attacker", - onlyAllowedIPAddresses: [ + allowedIPAddresses: [ { source: "geoip", description: "geo restrictions", diff --git a/library/sources/Hono.test.ts b/library/sources/Hono.test.ts index 79c097d2f..155e929a6 100644 --- a/library/sources/Hono.test.ts +++ b/library/sources/Hono.test.ts @@ -31,7 +31,7 @@ wrap(fetch, "fetch", function mock(original) { }, ], blockedUserAgents: "hacker|attacker", - onlyAllowedIPAddresses: [], + allowedIPAddresses: [], }), }; } diff --git a/library/sources/http-server/checkIfRequestIsBlocked.ts b/library/sources/http-server/checkIfRequestIsBlocked.ts index 9d1742ff1..527808c4c 100644 --- a/library/sources/http-server/checkIfRequestIsBlocked.ts +++ b/library/sources/http-server/checkIfRequestIsBlocked.ts @@ -4,7 +4,6 @@ import { Agent } from "../../agent/Agent"; import { getContext } from "../../agent/Context"; import { escapeHTML } from "../../helpers/escapeHTML"; import { ipAllowedToAccessRoute } from "./ipAllowedToAccessRoute"; -import { isPrivateIP } from "../../vulnerabilities/ssrf/isPrivateIP"; /** * Inspects the IP address of the request: @@ -51,9 +50,7 @@ export function checkIfRequestIsBlocked( if ( context.remoteAddress && - agent.getConfig().shouldOnlyAllowSomeIPAddresses() && - !isPrivateIP(context.remoteAddress) && - !agent.getConfig().isOnlyAllowedIPAddress(context.remoteAddress).allowed + !agent.getConfig().isAllowedIPAddress(context.remoteAddress).allowed ) { res.statusCode = 403; res.setHeader("Content-Type", "text/plain"); From 77fe7b028eb7d43f73610554af849a1ae8f79729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Thu, 13 Feb 2025 09:26:13 +0100 Subject: [PATCH 12/12] Improve allowed ip addresses test --- ...est.ts => Hono.allowedIPAddresses.test.ts} | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) rename library/sources/{Hono.onlyAllowIPAddresses.test.ts => Hono.allowedIPAddresses.test.ts} (85%) diff --git a/library/sources/Hono.onlyAllowIPAddresses.test.ts b/library/sources/Hono.allowedIPAddresses.test.ts similarity index 85% rename from library/sources/Hono.onlyAllowIPAddresses.test.ts rename to library/sources/Hono.allowedIPAddresses.test.ts index e6e5fd0d5..f51ce81b4 100644 --- a/library/sources/Hono.onlyAllowIPAddresses.test.ts +++ b/library/sources/Hono.allowedIPAddresses.test.ts @@ -61,7 +61,7 @@ const agent = createTestAgent({ blockedUserIds: ["567"], configUpdatedAt: 0, heartbeatIntervalInMS: 10 * 60 * 1000, - allowedIPAddresses: ["4.3.2.1"], + allowedIPAddresses: ["5.6.7.8"], }), }); agent.start([new HonoInternal(), new HTTPServer()]); @@ -124,5 +124,26 @@ t.test("test access only allowed for some IP addresses", opts, async (t) => { }); t.equal(response4.statusCode, 200); + const response5 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "11.9.8.7", + }, + }); + t.equal(response5.statusCode, 403); + t.equal( + response5.body, + "Your IP address is not allowed to access this resource. (Your IP: 11.9.8.7)" + ); + + // Allow bypased IP addresses + const response6 = await fetch.fetch({ + url: new URL("http://127.0.0.1:8768/"), + headers: { + "X-Forwarded-For": "5.6.7.8", + }, + }); + t.equal(response6.statusCode, 200); + server.close(); });