@liberstudio/cloudflare-list 2.3.2 → 2.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/dist/attacks.service.d.ts +1 -1
- package/dist/attacks.service.js +8 -0
- package/dist/interfaces/cloudflare-attacks.interface.d.ts +1 -0
- package/dist/utils/cloudflare-ips.util.d.ts +2 -0
- package/dist/utils/cloudflare-ips.util.js +106 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,14 +11,18 @@ Request → 404 rilevato dal middleware
|
|
|
11
11
|
└─ IP estratto (cf-connecting-ip / x-real-ip / x-forwarded-for)
|
|
12
12
|
└─ throttle 5s per IP
|
|
13
13
|
└─ log su file (se path non è silentPath)
|
|
14
|
-
└─
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
└─ allowedIps? → skip (log only)
|
|
15
|
+
└─ IP Cloudflare? → skip (log only)
|
|
16
|
+
└─ ASN lookup su api.hackertarget.com
|
|
17
|
+
├─ range trovato → blocca intera subnet su Cloudflare
|
|
18
|
+
└─ fallback → blocca singolo IP /32 o /128
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
### Filtraggio automatico
|
|
20
22
|
|
|
21
23
|
Il modulo ignora automaticamente:
|
|
24
|
+
- IP nella whitelist `allowedIps` (configurazione statica)
|
|
25
|
+
- IP appartenenti ai range ufficiali Cloudflare (scaricati e cachati 24h)
|
|
22
26
|
- IP privati (10.x, 192.168.x, 172.16–31.x)
|
|
23
27
|
- Loopback (127.0.0.1, ::1)
|
|
24
28
|
- IPv4-mapped IPv6 (::ffff:x.x.x.x → convertiti in IPv4)
|
|
@@ -68,6 +72,7 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
68
72
|
apiToken: "your-cloudflare-api-token",
|
|
69
73
|
comment: "Blocked by liberstudio/cloudflare-list",
|
|
70
74
|
logPath: "/var/log/nestjs-attacks.log",
|
|
75
|
+
allowedIps: ["1.2.3.4", "10.0.0.0/8"],
|
|
71
76
|
excludedPaths: ["/api/health", "/api/webhook"],
|
|
72
77
|
silentPaths: ["/auth/me", "/auth/refresh"],
|
|
73
78
|
verbose: false,
|
|
@@ -93,6 +98,7 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
93
98
|
listId: config.getOrThrow<string>("CLOUDFLARE_LIST_ID"),
|
|
94
99
|
comment: config.get<string>("CLOUDFLARE_LIST_COMMENT") || "Blocked",
|
|
95
100
|
logPath: config.get<string>("CLOUDFLARE_LIST_LOG_PATH") || "/var/log/nestjs-attacks.log",
|
|
101
|
+
allowedIps: ["1.2.3.4", "10.0.0.0/8"],
|
|
96
102
|
excludedPaths: ["/api/health", "/api/webhook", /^\/api\/public\/.*/],
|
|
97
103
|
silentPaths: ["/auth/me", "/auth/refresh"],
|
|
98
104
|
verbose: false,
|
|
@@ -114,10 +120,26 @@ export class AppModule {}
|
|
|
114
120
|
| `listId` | `string` | si | ID della IP List Cloudflare |
|
|
115
121
|
| `comment` | `string` | si | Commento aggiunto ad ogni IP bloccato |
|
|
116
122
|
| `logPath` | `string` | si | Percorso del file di log degli attacchi |
|
|
123
|
+
| `allowedIps` | `string[]` | no | IP o CIDR mai bloccati (whitelist, supporta IPv4 e IPv6) |
|
|
117
124
|
| `excludedPaths` | `(string \| RegExp)[]` | no | Path esclusi da ogni rilevamento e log |
|
|
118
125
|
| `silentPaths` | `(string \| RegExp)[]` | no | Path bloccati su Cloudflare ma senza log su file |
|
|
119
126
|
| `verbose` | `boolean` | no | Se `true`, il log include body, query, params e stack |
|
|
120
127
|
|
|
128
|
+
### `allowedIps` — whitelist server
|
|
129
|
+
|
|
130
|
+
Lista di IP o subnet CIDR che non vengono mai bloccati, indipendentemente dal comportamento. Utile per i propri server, proxy interni, CDN proprietarie. Supporta IPv4, CIDR IPv4, IPv6 e CIDR IPv6:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
allowedIps: [
|
|
134
|
+
"1.2.3.4", // IP singolo IPv4
|
|
135
|
+
"10.0.0.0/8", // subnet IPv4
|
|
136
|
+
"2001:db8::1", // IP singolo IPv6
|
|
137
|
+
"2001:db8::/32", // subnet IPv6
|
|
138
|
+
]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
> I range ufficiali Cloudflare sono esclusi automaticamente senza bisogno di aggiungerli qui.
|
|
142
|
+
|
|
121
143
|
### `excludedPaths` vs `silentPaths`
|
|
122
144
|
|
|
123
145
|
- **`excludedPaths`**: il path viene ignorato completamente — nessun log, nessun blocco. Usato per path infrastrutturali (`/health`, `/webhook`).
|
|
@@ -7,7 +7,7 @@ export declare class AttacksService {
|
|
|
7
7
|
private readonly API_URL;
|
|
8
8
|
private readonly ASN_LOOKUP_URL;
|
|
9
9
|
constructor(httpService: HttpService, options: CloudflareAttacksOptions);
|
|
10
|
-
blockIp(rawIp: string): Promise<CloudflareResponse>;
|
|
10
|
+
blockIp(rawIp: string): Promise<CloudflareResponse | null>;
|
|
11
11
|
private lookupAsnRange;
|
|
12
12
|
updateIpList(cidr: string): Promise<CloudflareResponse>;
|
|
13
13
|
}
|
package/dist/attacks.service.js
CHANGED
|
@@ -31,6 +31,14 @@ let AttacksService = class AttacksService {
|
|
|
31
31
|
this.ASN_LOOKUP_URL = `https://api.hackertarget.com/aslookup`;
|
|
32
32
|
}
|
|
33
33
|
async blockIp(rawIp) {
|
|
34
|
+
if ((0, utils_1.isAllowedIp)(rawIp, this.options.allowedIps ?? [])) {
|
|
35
|
+
this.logger.log(`IP ${rawIp} in whitelist — ban saltato`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (await (0, utils_1.isCloudflareIp)(rawIp)) {
|
|
39
|
+
this.logger.log(`IP ${rawIp} appartiene al range Cloudflare — ban saltato`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
34
42
|
const asnRange = await this.lookupAsnRange(rawIp);
|
|
35
43
|
if (asnRange) {
|
|
36
44
|
this.logger.log(`ASN range trovato per ${rawIp}: ${asnRange} — blocco intera subnet`);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isAllowedIp = isAllowedIp;
|
|
7
|
+
exports.isCloudflareIp = isCloudflareIp;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const net_1 = require("net");
|
|
10
|
+
const CF_IPS_V4_URL = "https://www.cloudflare.com/ips-v4/";
|
|
11
|
+
const CF_IPS_V6_URL = "https://www.cloudflare.com/ips-v6/";
|
|
12
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
let cache = null;
|
|
14
|
+
let inFlight = null;
|
|
15
|
+
async function loadCloudflareIps() {
|
|
16
|
+
if (cache && Date.now() < cache.expiresAt)
|
|
17
|
+
return cache;
|
|
18
|
+
if (inFlight)
|
|
19
|
+
return inFlight;
|
|
20
|
+
inFlight = (async () => {
|
|
21
|
+
const [v4Res, v6Res] = await Promise.all([
|
|
22
|
+
axios_1.default.get(CF_IPS_V4_URL, { responseType: "text" }),
|
|
23
|
+
axios_1.default.get(CF_IPS_V6_URL, { responseType: "text" }),
|
|
24
|
+
]);
|
|
25
|
+
const parse = (data) => data.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
26
|
+
cache = {
|
|
27
|
+
v4: parse(v4Res.data),
|
|
28
|
+
v6: parse(v6Res.data),
|
|
29
|
+
expiresAt: Date.now() + TTL_MS,
|
|
30
|
+
};
|
|
31
|
+
return cache;
|
|
32
|
+
})().finally(() => {
|
|
33
|
+
inFlight = null;
|
|
34
|
+
});
|
|
35
|
+
return inFlight;
|
|
36
|
+
}
|
|
37
|
+
function ipv4ToInt(ip) {
|
|
38
|
+
return ip.split(".").reduce((acc, oct) => (acc << 8) + Number(oct), 0) >>> 0;
|
|
39
|
+
}
|
|
40
|
+
function ipv6ToBigInt(ip) {
|
|
41
|
+
const [headStr, tailStr] = ip.includes("::") ? ip.split("::") : [ip, ""];
|
|
42
|
+
const head = headStr ? headStr.split(":") : [];
|
|
43
|
+
const tail = tailStr ? tailStr.split(":") : [];
|
|
44
|
+
const missing = 8 - head.length - tail.length;
|
|
45
|
+
const groups = [...head, ...Array(missing).fill("0"), ...tail];
|
|
46
|
+
return groups.reduce((acc, g) => (acc << 16n) + BigInt(parseInt(g || "0", 16)), 0n);
|
|
47
|
+
}
|
|
48
|
+
function isInCidrV4(ip, cidr) {
|
|
49
|
+
const [range, bitsStr] = cidr.split("/");
|
|
50
|
+
const bits = bitsStr === undefined ? 32 : Number(bitsStr);
|
|
51
|
+
if (bits === 0)
|
|
52
|
+
return true;
|
|
53
|
+
const mask = (0xffffffff << (32 - bits)) >>> 0;
|
|
54
|
+
return (ipv4ToInt(ip) & mask) === (ipv4ToInt(range) & mask);
|
|
55
|
+
}
|
|
56
|
+
function isInCidrV6(ip, cidr) {
|
|
57
|
+
const [range, bitsStr] = cidr.split("/");
|
|
58
|
+
const prefix = bitsStr === undefined ? 128 : Number(bitsStr);
|
|
59
|
+
if (prefix === 0)
|
|
60
|
+
return true;
|
|
61
|
+
const mask = ((1n << 128n) - 1n) ^ ((1n << BigInt(128 - prefix)) - 1n);
|
|
62
|
+
return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
|
|
63
|
+
}
|
|
64
|
+
function isAllowedIp(ip, allowedIps) {
|
|
65
|
+
if (!ip || !allowedIps.length)
|
|
66
|
+
return false;
|
|
67
|
+
let pure = ip.includes("/") ? ip.split("/")[0] : ip;
|
|
68
|
+
pure = pure.trim();
|
|
69
|
+
if (pure.startsWith("::ffff:"))
|
|
70
|
+
pure = pure.slice("::ffff:".length);
|
|
71
|
+
const version = (0, net_1.isIP)(pure);
|
|
72
|
+
if (!version)
|
|
73
|
+
return false;
|
|
74
|
+
return allowedIps.some((entry) => {
|
|
75
|
+
let entryPure = entry.includes("/") ? entry.split("/")[0] : entry;
|
|
76
|
+
entryPure = entryPure.trim();
|
|
77
|
+
if (entryPure.startsWith("::ffff:"))
|
|
78
|
+
entryPure = entryPure.slice("::ffff:".length);
|
|
79
|
+
const entryVersion = (0, net_1.isIP)(entryPure);
|
|
80
|
+
if (!entryVersion || entryVersion !== version)
|
|
81
|
+
return false;
|
|
82
|
+
return version === 4
|
|
83
|
+
? isInCidrV4(pure, entry)
|
|
84
|
+
: isInCidrV6(pure, entry);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function isCloudflareIp(ip) {
|
|
88
|
+
if (!ip)
|
|
89
|
+
return false;
|
|
90
|
+
let pure = ip.includes("/") ? ip.split("/")[0] : ip;
|
|
91
|
+
pure = pure.trim();
|
|
92
|
+
if (pure.startsWith("::ffff:"))
|
|
93
|
+
pure = pure.slice("::ffff:".length);
|
|
94
|
+
const version = (0, net_1.isIP)(pure);
|
|
95
|
+
if (!version)
|
|
96
|
+
return false;
|
|
97
|
+
try {
|
|
98
|
+
const { v4, v6 } = await loadCloudflareIps();
|
|
99
|
+
return version === 4
|
|
100
|
+
? v4.some((cidr) => isInCidrV4(pure, cidr))
|
|
101
|
+
: v6.some((cidr) => isInCidrV6(pure, cidr));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -15,5 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./cloudflare.constants"), exports);
|
|
18
|
+
__exportStar(require("./cloudflare-ips.util"), exports);
|
|
18
19
|
__exportStar(require("./get-client-ip.util"), exports);
|
|
19
20
|
__exportStar(require("./options.validator"), exports);
|