Skip to content

Commit 8fb0f50

Browse files
authored
Merge pull request dubinc#2366 from dubinc/bot-ip-range
Add IP_RANGES_BOTS
2 parents 7ecc1ee + 04f5425 commit 8fb0f50

4 files changed

Lines changed: 113 additions & 4 deletions

File tree

apps/web/lib/middleware/utils/bots-list.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const UA_BOTS = [
3333
"cron-job",
3434
"InternetMeasurement",
3535
"HostTracker",
36+
"Expanse", // Expanse (Palo Alto Networks)
3637

3738
// AI bots
3839
"anthropic-ai", // Anthropic AI
@@ -90,3 +91,11 @@ export const IP_BOTS = [
9091
"34.105.67.76", // The Dalles
9192
"154.28.229.7", // Ashburn
9293
];
94+
95+
export const IP_RANGES_BOTS = [
96+
"159.148.128.0/24", // weird bot activity from Miami
97+
98+
// Expanse (Palo Alto Networks)
99+
"198.235.24.0/24",
100+
"205.210.31.0/24",
101+
];

apps/web/lib/middleware/utils/detect-bot.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ipAddress } from "@vercel/functions";
22
import { userAgent } from "next/server";
3-
import { IP_BOTS, UA_BOTS } from "./bots-list";
3+
import { IP_BOTS, IP_RANGES_BOTS, UA_BOTS } from "./bots-list";
4+
import { isIpInRange } from "./is-ip-in-range";
45

56
export const detectBot = (req: Request) => {
67
const searchParams = new URL(req.url).searchParams;
@@ -23,9 +24,11 @@ export const detectBot = (req: Request) => {
2324
return false;
2425
}
2526

26-
if (ip.includes("/")) {
27-
ip = ip.split("/")[0];
27+
// Check exact IP matches
28+
if (IP_BOTS.includes(ip)) {
29+
return true;
2830
}
2931

30-
return IP_BOTS.includes(ip);
32+
// Check CIDR ranges
33+
return IP_RANGES_BOTS.some((range) => isIpInRange(ip, range));
3134
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Helper function to check if an IP is in a CIDR range
2+
export const isIpInRange = (ip: string, cidr: string): boolean => {
3+
// Validate CIDR format
4+
const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/;
5+
if (!cidrRegex.test(cidr)) {
6+
return false;
7+
}
8+
9+
const [rangeIp, prefix] = cidr.split("/");
10+
const prefixLength = parseInt(prefix);
11+
12+
// Validate prefix length
13+
if (prefixLength < 0 || prefixLength > 32) {
14+
return false;
15+
}
16+
17+
// Validate IP format
18+
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
19+
if (!ipRegex.test(ip)) {
20+
return false;
21+
}
22+
23+
// Convert IPs to binary
24+
const ipToBinary = (ip: string) => {
25+
return ip
26+
.split(".")
27+
.map((octet) => {
28+
const num = parseInt(octet);
29+
// Validate octet range
30+
if (num < 0 || num > 255) {
31+
throw new Error("Invalid IP octet");
32+
}
33+
return num.toString(2).padStart(8, "0");
34+
})
35+
.join("");
36+
};
37+
38+
try {
39+
const ipBinary = ipToBinary(ip);
40+
const rangeBinary = ipToBinary(rangeIp);
41+
42+
// Compare the network portions
43+
return (
44+
ipBinary.slice(0, prefixLength) === rangeBinary.slice(0, prefixLength)
45+
);
46+
} catch {
47+
return false;
48+
}
49+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { isIpInRange } from "@/lib/middleware/utils/is-ip-in-range";
2+
import { describe, expect, it } from "vitest";
3+
4+
describe("CIDR Range Checking", () => {
5+
describe("isIpInRange", () => {
6+
it("should return true for IPs in the range", () => {
7+
// Test with /24 range (256 IPs)
8+
expect(isIpInRange("159.148.128.0", "159.148.128.0/24")).toBe(true);
9+
expect(isIpInRange("159.148.128.1", "159.148.128.0/24")).toBe(true);
10+
expect(isIpInRange("159.148.128.255", "159.148.128.0/24")).toBe(true);
11+
12+
// Test with /16 range (65,536 IPs)
13+
expect(isIpInRange("159.148.0.0", "159.148.0.0/16")).toBe(true);
14+
expect(isIpInRange("159.148.255.255", "159.148.0.0/16")).toBe(true);
15+
});
16+
17+
it("should return false for IPs outside the range", () => {
18+
// Test with /24 range
19+
expect(isIpInRange("159.148.127.255", "159.148.128.0/24")).toBe(false);
20+
expect(isIpInRange("159.148.129.0", "159.148.128.0/24")).toBe(false);
21+
22+
// Test with /16 range
23+
expect(isIpInRange("159.147.255.255", "159.148.0.0/16")).toBe(false);
24+
expect(isIpInRange("159.149.0.0", "159.148.0.0/16")).toBe(false);
25+
});
26+
27+
it("should handle different CIDR prefix lengths", () => {
28+
// Test with /32 (single IP)
29+
expect(isIpInRange("192.168.1.1", "192.168.1.1/32")).toBe(true);
30+
expect(isIpInRange("192.168.1.2", "192.168.1.1/32")).toBe(false);
31+
32+
// Test with /8 (16,777,216 IPs)
33+
expect(isIpInRange("10.0.0.0", "10.0.0.0/8")).toBe(true);
34+
expect(isIpInRange("10.255.255.255", "10.0.0.0/8")).toBe(true);
35+
expect(isIpInRange("11.0.0.0", "10.0.0.0/8")).toBe(false);
36+
});
37+
38+
it("should handle edge cases", () => {
39+
// Test with 0.0.0.0
40+
expect(isIpInRange("0.0.0.0", "0.0.0.0/0")).toBe(true);
41+
expect(isIpInRange("255.255.255.255", "0.0.0.0/0")).toBe(true);
42+
43+
// Test with invalid inputs
44+
expect(isIpInRange("invalid-ip", "159.148.128.0/24")).toBe(false);
45+
expect(isIpInRange("159.148.128.0", "invalid-cidr")).toBe(false);
46+
});
47+
});
48+
});

0 commit comments

Comments
 (0)