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 437386a7b..87797b8e3 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/agent/Agent.test.ts b/library/agent/Agent.test.ts
index 358b9fc6b..6f1898db6 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,10 @@ t.test("it does not fetch blocked IPs if serverless", async () => {
blocked: false,
});
+ t.same(agent.getConfig().isAllowedIPAddress("1.3.2.4"), {
+ allowed: true,
+ });
+
t.same(
agent
.getConfig()
@@ -1110,3 +1125,31 @@ 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().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 5698419d0..c3a596331 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({
@@ -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.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 b4144cb26..cf8e192f8 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.isBypassedIP("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 bypassed", async () => {
- const config = new ServiceConfig([], 0, [], ["1.2.3.4"], false, []);
+ const config = new ServiceConfig([], 0, [], ["1.2.3.4"], false, [], []);
t.same(config.isBypassedIP("1.2.3.4"), true);
t.same(config.isBypassedIP("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,7 +141,7 @@ 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 });
@@ -143,3 +152,51 @@ t.test("it blocks bots", async () => {
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.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.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 () => {
+ const config = new ServiceConfig(
+ [],
+ 0,
+ [],
+ [],
+ true,
+ [],
+ [
+ {
+ source: "geoip",
+ description: "description",
+ ips: [],
+ },
+ ]
+ );
+
+ 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 d10cf96d3..b770fa0bd 100644
--- a/library/agent/ServiceConfig.ts
+++ b/library/agent/ServiceConfig.ts
@@ -1,16 +1,24 @@
import { IPMatcher } from "../helpers/ip-matcher/IPMatcher";
import { LimitedContext, matchEndpoints } from "../helpers/matchEndpoints";
+import { isPrivateIP } from "../vulnerabilities/ssrf/isPrivateIP";
import { Endpoint } from "./Config";
-import { Blocklist as BlocklistType } from "./api/fetchBlockedLists";
+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 allowedIPAddresses: {
+ allowlist: IPMatcher;
+ description: string;
+ }[] = [];
constructor(
endpoints: Endpoint[],
@@ -18,12 +26,14 @@ export class ServiceConfig {
blockedUserIds: string[],
bypassedIPAddresses: string[],
private receivedAnyStats: boolean,
- blockedIPAddresses: BlocklistType[]
+ blockedIPAddresses: IPList[],
+ allowedIPAddresses: IPList[]
) {
this.setBlockedUserIds(blockedUserIds);
this.setBypassedIPAddresses(bypassedIPAddresses);
this.setEndpoints(endpoints);
this.setBlockedIPAddresses(blockedIPAddresses);
+ this.setAllowedIPAddresses(allowedIPAddresses);
}
private setEndpoints(endpoints: Endpoint[]) {
@@ -96,7 +106,7 @@ export class ServiceConfig {
return { blocked: false };
}
- private setBlockedIPAddresses(blockedIPAddresses: BlocklistType[]) {
+ private setBlockedIPAddresses(blockedIPAddresses: IPList[]) {
this.blockedIPAddresses = [];
for (const source of blockedIPAddresses) {
@@ -107,7 +117,7 @@ export class ServiceConfig {
}
}
- updateBlockedIPAddresses(blockedIPAddresses: BlocklistType[]) {
+ updateBlockedIPAddresses(blockedIPAddresses: IPList[]) {
this.setBlockedIPAddresses(blockedIPAddresses);
}
@@ -126,6 +136,42 @@ export class ServiceConfig {
return { blocked: false };
}
+ private setAllowedIPAddresses(ipAddresses: IPList[]) {
+ this.allowedIPAddresses = [];
+
+ for (const source of ipAddresses) {
+ // Skip empty allowlists
+ if (source.ips.length === 0) {
+ continue;
+ }
+ this.allowedIPAddresses.push({
+ allowlist: new IPMatcher(source.ips),
+ description: source.description,
+ });
+ }
+ }
+
+ updateAllowedIPAddresses(ipAddresses: IPList[]) {
+ this.setAllowedIPAddresses(ipAddresses);
+ }
+
+ 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 };
+ }
+
+ const allowlist = this.allowedIPAddresses.find((list) =>
+ list.allowlist.has(ip)
+ );
+
+ return { allowed: !!allowlist };
+ }
+
updateConfig(
endpoints: Endpoint[],
lastUpdatedAt: number,
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/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 f41edad7a..922b902b8 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.onlyAllowIPAddresses.test.ts b/library/sources/Hono.onlyAllowIPAddresses.test.ts
new file mode 100644
index 000000000..e6e5fd0d5
--- /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",
+ allowedIPAddresses: [
+ {
+ 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 b6a8a9120..155e929a6 100644
--- a/library/sources/Hono.test.ts
+++ b/library/sources/Hono.test.ts
@@ -31,6 +31,7 @@ wrap(fetch, "fetch", function mock(original) {
},
],
blockedUserAgents: "hacker|attacker",
+ allowedIPAddresses: [],
}),
};
}
diff --git a/library/sources/http-server/checkIfRequestIsBlocked.ts b/library/sources/http-server/checkIfRequestIsBlocked.ts
index 0f3be8732..527808c4c 100644
--- a/library/sources/http-server/checkIfRequestIsBlocked.ts
+++ b/library/sources/http-server/checkIfRequestIsBlocked.ts
@@ -48,6 +48,23 @@ export function checkIfRequestIsBlocked(
return false;
}
+ if (
+ context.remoteAddress &&
+ !agent.getConfig().isAllowedIPAddress(context.remoteAddress).allowed
+ ) {
+ 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);