@opensecurity/zonzon-core 0.1.4 → 0.1.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/dist/audit.d.ts +4 -0
- package/dist/audit.js +116 -10
- package/dist/cache-layer.test.js +1 -1
- package/dist/cache-multi-question.test.js +1 -1
- package/dist/dns-handler.d.ts +9 -0
- package/dist/dns-handler.js +308 -36
- package/dist/dns-service.d.ts +0 -1
- package/dist/dns-service.js +8 -17
- package/dist/dns-service.test.js +1 -1
- package/dist/dns-wireformat.test.js +1 -1
- package/dist/doh-dot.test.d.ts +1 -0
- package/dist/doh-dot.test.js +104 -0
- package/dist/firewall.d.ts +2 -0
- package/dist/firewall.js +68 -9
- package/dist/firewall.test.d.ts +1 -0
- package/dist/firewall.test.js +165 -0
- package/dist/http-body-forwarding-integration.test.js +16 -1
- package/dist/http-body-forwarding.test.js +1 -1
- package/dist/http-handler.d.ts +3 -1
- package/dist/http-handler.js +175 -135
- package/dist/http-proxy.d.ts +1 -2
- package/dist/http-proxy.js +7 -9
- package/dist/rate-limiter.d.ts +5 -0
- package/dist/rate-limiter.js +30 -11
- package/dist/rate-limiter.test.js +2 -2
- package/dist/schema.js +14 -7
- package/dist/sni-proxy.d.ts +1 -0
- package/dist/sni-proxy.js +61 -11
- package/dist/sni-proxy.test.d.ts +1 -0
- package/dist/sni-proxy.test.js +69 -0
- package/dist/srv-record.test.js +1 -1
- package/dist/tcp-connection-limit.test.js +2 -2
- package/dist/types.d.ts +8 -0
- package/dist/wildcard-matching.test.js +1 -1
- package/package.json +2 -2
package/dist/dns-service.js
CHANGED
|
@@ -245,7 +245,6 @@ function buildNoErrorResponse(id, flags, questions) {
|
|
|
245
245
|
export class DevDnsServer {
|
|
246
246
|
config;
|
|
247
247
|
cacheMap = new Map();
|
|
248
|
-
cacheOrder = [];
|
|
249
248
|
dnsCacheMaxSize;
|
|
250
249
|
dnsCacheTtlMs;
|
|
251
250
|
constructor(config) {
|
|
@@ -279,10 +278,10 @@ export class DevDnsServer {
|
|
|
279
278
|
return questions.map((q) => `${q.name}:${q.type}`).join("|");
|
|
280
279
|
}
|
|
281
280
|
evictCacheEntry() {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
const oldestKey = this.cacheMap.keys().next().value;
|
|
282
|
+
if (oldestKey !== undefined) {
|
|
283
|
+
this.cacheMap.delete(oldestKey);
|
|
284
|
+
}
|
|
286
285
|
}
|
|
287
286
|
getFromCache(key) {
|
|
288
287
|
const entry = this.cacheMap.get(key);
|
|
@@ -290,15 +289,10 @@ export class DevDnsServer {
|
|
|
290
289
|
return null;
|
|
291
290
|
if (this.dnsCacheTtlMs > 0 && Date.now() - entry.insertedAt >= this.dnsCacheTtlMs) {
|
|
292
291
|
this.cacheMap.delete(key);
|
|
293
|
-
const idx = this.cacheOrder.indexOf(key);
|
|
294
|
-
if (idx >= 0)
|
|
295
|
-
this.cacheOrder.splice(idx, 1);
|
|
296
292
|
return null;
|
|
297
293
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
this.cacheOrder.splice(idx, 1);
|
|
301
|
-
this.cacheOrder.push(key);
|
|
294
|
+
this.cacheMap.delete(key);
|
|
295
|
+
this.cacheMap.set(key, entry);
|
|
302
296
|
const remainingTtlMs = Math.max(0, this.dnsCacheTtlMs - (Date.now() - entry.insertedAt));
|
|
303
297
|
entry.ttlMs = remainingTtlMs;
|
|
304
298
|
return entry;
|
|
@@ -306,7 +300,7 @@ export class DevDnsServer {
|
|
|
306
300
|
addToCache(key, response) {
|
|
307
301
|
if (this.dnsCacheTtlMs <= 0)
|
|
308
302
|
return;
|
|
309
|
-
while (this.
|
|
303
|
+
while (this.cacheMap.size >= this.dnsCacheMaxSize) {
|
|
310
304
|
this.evictCacheEntry();
|
|
311
305
|
}
|
|
312
306
|
const remainingSeconds = Math.max(1, Math.floor(this.dnsCacheTtlMs / 1000));
|
|
@@ -316,11 +310,8 @@ export class DevDnsServer {
|
|
|
316
310
|
insertedAt: Date.now(),
|
|
317
311
|
ttlMs: this.dnsCacheTtlMs,
|
|
318
312
|
};
|
|
313
|
+
this.cacheMap.delete(key);
|
|
319
314
|
this.cacheMap.set(key, entry);
|
|
320
|
-
const idx = this.cacheOrder.indexOf(key);
|
|
321
|
-
if (idx >= 0)
|
|
322
|
-
this.cacheOrder.splice(idx, 1);
|
|
323
|
-
this.cacheOrder.push(key);
|
|
324
315
|
}
|
|
325
316
|
skipQuestionSection(buffer, offset) {
|
|
326
317
|
const savedOffset = offset;
|
package/dist/dns-service.test.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { DnsHandler } from "./dns-handler.js";
|
|
4
|
+
import { DevDnsServer } from "./dns-service.js";
|
|
5
|
+
function createMockReqRes(method, url, headers = {}) {
|
|
6
|
+
const req = {
|
|
7
|
+
method,
|
|
8
|
+
url,
|
|
9
|
+
headers,
|
|
10
|
+
socket: { remoteAddress: "127.0.0.1" },
|
|
11
|
+
destroy: () => { }
|
|
12
|
+
};
|
|
13
|
+
let statusCode = 0;
|
|
14
|
+
let endData = null;
|
|
15
|
+
const resHeaders = {};
|
|
16
|
+
const res = {
|
|
17
|
+
writeHead: (code, headers) => {
|
|
18
|
+
statusCode = code;
|
|
19
|
+
if (headers)
|
|
20
|
+
Object.assign(resHeaders, headers);
|
|
21
|
+
},
|
|
22
|
+
end: (data) => {
|
|
23
|
+
endData = data;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return { req, res, getStatus: () => statusCode, getEndData: () => endData, getHeaders: () => resHeaders };
|
|
27
|
+
}
|
|
28
|
+
describe("Modern DNS Protocols (DoH)", () => {
|
|
29
|
+
const config = {
|
|
30
|
+
port: 53,
|
|
31
|
+
hosts: {}
|
|
32
|
+
};
|
|
33
|
+
const server = new DevDnsServer(config);
|
|
34
|
+
const handler = new DnsHandler(server, config);
|
|
35
|
+
it("rejects DoH request on invalid path", async () => {
|
|
36
|
+
const { req, res, getStatus } = createMockReqRes("GET", "/invalid-path");
|
|
37
|
+
// @ts-ignore - Accessing private method for strict L7 unit testing
|
|
38
|
+
await handler.handleDohRequest(req, res);
|
|
39
|
+
assert.strictEqual(getStatus(), 404);
|
|
40
|
+
});
|
|
41
|
+
it("rejects DoH GET request missing dns query parameter", async () => {
|
|
42
|
+
const { req, res, getStatus } = createMockReqRes("GET", "/dns-query");
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
await handler.handleDohRequest(req, res);
|
|
45
|
+
assert.strictEqual(getStatus(), 400);
|
|
46
|
+
});
|
|
47
|
+
it("rejects DoH GET request with invalid base64url payload", async () => {
|
|
48
|
+
const { req, res, getStatus } = createMockReqRes("GET", "/dns-query?dns=!!!invalid_base64!!!");
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
await handler.handleDohRequest(req, res);
|
|
51
|
+
assert.strictEqual(getStatus(), 400);
|
|
52
|
+
});
|
|
53
|
+
it("rejects DoH POST request with invalid content-type", async () => {
|
|
54
|
+
const { req, res, getStatus } = createMockReqRes("POST", "/dns-query", {
|
|
55
|
+
"content-type": "application/json"
|
|
56
|
+
});
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
await handler.handleDohRequest(req, res);
|
|
59
|
+
assert.strictEqual(getStatus(), 415);
|
|
60
|
+
});
|
|
61
|
+
it("rejects unsupported HTTP methods", async () => {
|
|
62
|
+
const { req, res, getStatus } = createMockReqRes("PUT", "/dns-query");
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
await handler.handleDohRequest(req, res);
|
|
65
|
+
assert.strictEqual(getStatus(), 405);
|
|
66
|
+
});
|
|
67
|
+
it("rejects payloads that are too short to be valid DNS packets", async () => {
|
|
68
|
+
const { req, res, getStatus } = createMockReqRes("GET", "/dns-query?dns=abcd");
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
await handler.handleDohRequest(req, res);
|
|
71
|
+
assert.strictEqual(getStatus(), 400);
|
|
72
|
+
});
|
|
73
|
+
it("enforces memory bounds on DoH POST payloads to prevent exhaustion", async () => {
|
|
74
|
+
const { req, res, getStatus } = createMockReqRes("POST", "/dns-query", {
|
|
75
|
+
"content-type": "application/dns-message"
|
|
76
|
+
});
|
|
77
|
+
const oversizedBuffer = Buffer.alloc(65 * 1024); // Exceeds MAX_TCP_BUFFER_SIZE (64KB)
|
|
78
|
+
req[Symbol.asyncIterator] = async function* () {
|
|
79
|
+
yield oversizedBuffer;
|
|
80
|
+
};
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
await handler.handleDohRequest(req, res);
|
|
83
|
+
assert.strictEqual(getStatus(), 413);
|
|
84
|
+
});
|
|
85
|
+
it("processes a structurally valid DoH POST request", async () => {
|
|
86
|
+
const { req, res, getStatus, getHeaders } = createMockReqRes("POST", "/dns-query", {
|
|
87
|
+
"content-type": "application/dns-message"
|
|
88
|
+
});
|
|
89
|
+
// Create a minimal 12-byte valid DNS header
|
|
90
|
+
const validDnsHeader = Buffer.alloc(12);
|
|
91
|
+
validDnsHeader.writeUInt16BE(0x1234, 0); // ID
|
|
92
|
+
validDnsHeader.writeUInt16BE(0x0100, 2); // Flags (Standard Query)
|
|
93
|
+
validDnsHeader.writeUInt16BE(0x0000, 4); // QDCOUNT
|
|
94
|
+
req[Symbol.asyncIterator] = async function* () {
|
|
95
|
+
yield validDnsHeader;
|
|
96
|
+
};
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
await handler.handleDohRequest(req, res);
|
|
99
|
+
assert.strictEqual(getStatus(), 200);
|
|
100
|
+
const headers = getHeaders();
|
|
101
|
+
assert.strictEqual(headers["Content-Type"], "application/dns-message");
|
|
102
|
+
assert.strictEqual(headers["Cache-Control"], "max-age=0");
|
|
103
|
+
});
|
|
104
|
+
});
|
package/dist/firewall.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare class FirewallEngine {
|
|
|
3
3
|
private ipToInt;
|
|
4
4
|
private matchCidr;
|
|
5
5
|
private matchDomain;
|
|
6
|
+
isRestrictedOutbound(ip: string): boolean;
|
|
7
|
+
evaluateOutbound(ip: string, fw?: FirewallConfig): "ALLOW" | "DENY";
|
|
6
8
|
evaluateIp(ip: string, fw?: FirewallConfig): "ALLOW" | "DENY";
|
|
7
9
|
evaluateDomain(domain: string, fw?: FirewallConfig): "ALLOW" | "DENY";
|
|
8
10
|
}
|
package/dist/firewall.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as net from "net";
|
|
1
|
+
import * as net from "node:net";
|
|
2
2
|
export class FirewallEngine {
|
|
3
3
|
ipToInt(ip) {
|
|
4
4
|
return ip.split('.').reduce((int, octet) => (int << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
@@ -22,26 +22,85 @@ export class FirewallEngine {
|
|
|
22
22
|
return true;
|
|
23
23
|
if (normPattern.startsWith("*.")) {
|
|
24
24
|
const suffix = normPattern.slice(2);
|
|
25
|
-
return normDomain
|
|
25
|
+
return normDomain.endsWith("." + suffix);
|
|
26
26
|
}
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
|
+
isRestrictedOutbound(ip) {
|
|
30
|
+
if (net.isIPv6(ip)) {
|
|
31
|
+
const normalized = ip.toLowerCase();
|
|
32
|
+
if (normalized === "::1")
|
|
33
|
+
return true;
|
|
34
|
+
if (normalized === "::")
|
|
35
|
+
return true;
|
|
36
|
+
if (normalized.startsWith("fe80:"))
|
|
37
|
+
return true;
|
|
38
|
+
if (normalized.startsWith("fc00:") || normalized.startsWith("fd"))
|
|
39
|
+
return true;
|
|
40
|
+
if (normalized.includes("::ffff:127."))
|
|
41
|
+
return true;
|
|
42
|
+
if (normalized.includes("::ffff:169.254."))
|
|
43
|
+
return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (!net.isIPv4(ip))
|
|
47
|
+
return true;
|
|
48
|
+
const parts = ip.split('.').map(Number);
|
|
49
|
+
if (parts[0] === 0)
|
|
50
|
+
return true;
|
|
51
|
+
if (parts[0] === 10)
|
|
52
|
+
return true;
|
|
53
|
+
if (parts[0] === 127)
|
|
54
|
+
return true;
|
|
55
|
+
if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127)
|
|
56
|
+
return true;
|
|
57
|
+
if (parts[0] === 169 && parts[1] === 254)
|
|
58
|
+
return true;
|
|
59
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
60
|
+
return true;
|
|
61
|
+
if (parts[0] === 192 && parts[1] === 168)
|
|
62
|
+
return true;
|
|
63
|
+
if (parts[0] >= 224 && parts[0] <= 239)
|
|
64
|
+
return true;
|
|
65
|
+
if (parts[0] >= 240 && parts[0] <= 255)
|
|
66
|
+
return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
evaluateOutbound(ip, fw) {
|
|
70
|
+
if (fw) {
|
|
71
|
+
if (fw.allowlist_ips && fw.allowlist_ips.includes(ip))
|
|
72
|
+
return "ALLOW";
|
|
73
|
+
if (net.isIPv4(ip)) {
|
|
74
|
+
for (const range of fw.allowlist_ranges || []) {
|
|
75
|
+
if (this.matchCidr(ip, range))
|
|
76
|
+
return "ALLOW";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (this.isRestrictedOutbound(ip))
|
|
81
|
+
return "DENY";
|
|
82
|
+
return "ALLOW";
|
|
83
|
+
}
|
|
29
84
|
evaluateIp(ip, fw) {
|
|
30
85
|
if (!fw)
|
|
31
86
|
return "ALLOW";
|
|
32
|
-
if (!net.isIPv4(ip))
|
|
87
|
+
if (!net.isIPv4(ip) && !net.isIPv6(ip))
|
|
33
88
|
return "DENY";
|
|
34
89
|
if (fw.blocklist_ips && fw.blocklist_ips.includes(ip))
|
|
35
90
|
return "DENY";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
if (net.isIPv4(ip)) {
|
|
92
|
+
for (const range of fw.blocklist_ranges || []) {
|
|
93
|
+
if (this.matchCidr(ip, range))
|
|
94
|
+
return "DENY";
|
|
95
|
+
}
|
|
39
96
|
}
|
|
40
97
|
if (fw.allowlist_ips && fw.allowlist_ips.includes(ip))
|
|
41
98
|
return "ALLOW";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
99
|
+
if (net.isIPv4(ip)) {
|
|
100
|
+
for (const range of fw.allowlist_ranges || []) {
|
|
101
|
+
if (this.matchCidr(ip, range))
|
|
102
|
+
return "ALLOW";
|
|
103
|
+
}
|
|
45
104
|
}
|
|
46
105
|
return fw.defaultPolicy === "allow" ? "ALLOW" : "DENY";
|
|
47
106
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { FirewallEngine } from "./firewall.js";
|
|
4
|
+
describe("FirewallEngine - Domain Evaluation", () => {
|
|
5
|
+
const engine = new FirewallEngine();
|
|
6
|
+
it("returns ALLOW when firewall config is undefined", () => {
|
|
7
|
+
assert.strictEqual(engine.evaluateDomain("example.com"), "ALLOW");
|
|
8
|
+
});
|
|
9
|
+
it("respects default deny policy", () => {
|
|
10
|
+
const config = { defaultPolicy: "deny" };
|
|
11
|
+
assert.strictEqual(engine.evaluateDomain("example.com", config), "DENY");
|
|
12
|
+
});
|
|
13
|
+
it("respects default allow policy", () => {
|
|
14
|
+
const config = { defaultPolicy: "allow" };
|
|
15
|
+
assert.strictEqual(engine.evaluateDomain("example.com", config), "ALLOW");
|
|
16
|
+
});
|
|
17
|
+
it("blocks explicitly blocklisted domains", () => {
|
|
18
|
+
const config = {
|
|
19
|
+
defaultPolicy: "allow",
|
|
20
|
+
blocklist_domains: ["evil.com"],
|
|
21
|
+
};
|
|
22
|
+
assert.strictEqual(engine.evaluateDomain("evil.com", config), "DENY");
|
|
23
|
+
});
|
|
24
|
+
it("allows explicitly allowlisted domains overriding default deny", () => {
|
|
25
|
+
const config = {
|
|
26
|
+
defaultPolicy: "deny",
|
|
27
|
+
allowlist_domains: ["good.com"],
|
|
28
|
+
};
|
|
29
|
+
assert.strictEqual(engine.evaluateDomain("good.com", config), "ALLOW");
|
|
30
|
+
});
|
|
31
|
+
it("prioritizes blocklist over allowlist", () => {
|
|
32
|
+
const config = {
|
|
33
|
+
defaultPolicy: "allow",
|
|
34
|
+
allowlist_domains: ["conflict.com"],
|
|
35
|
+
blocklist_domains: ["conflict.com"],
|
|
36
|
+
};
|
|
37
|
+
assert.strictEqual(engine.evaluateDomain("conflict.com", config), "DENY");
|
|
38
|
+
});
|
|
39
|
+
it("supports exact domain matching case-insensitively", () => {
|
|
40
|
+
const config = {
|
|
41
|
+
defaultPolicy: "deny",
|
|
42
|
+
allowlist_domains: ["secure.loop"],
|
|
43
|
+
};
|
|
44
|
+
assert.strictEqual(engine.evaluateDomain("SECURE.LOOP", config), "ALLOW");
|
|
45
|
+
});
|
|
46
|
+
it("supports wildcard domain matching", () => {
|
|
47
|
+
const config = {
|
|
48
|
+
defaultPolicy: "deny",
|
|
49
|
+
allowlist_domains: ["*.internal.net"],
|
|
50
|
+
};
|
|
51
|
+
assert.strictEqual(engine.evaluateDomain("api.internal.net", config), "ALLOW");
|
|
52
|
+
assert.strictEqual(engine.evaluateDomain("db.internal.net", config), "ALLOW");
|
|
53
|
+
assert.strictEqual(engine.evaluateDomain("internal.net", config), "DENY");
|
|
54
|
+
});
|
|
55
|
+
it("handles trailing dots seamlessly", () => {
|
|
56
|
+
const config = {
|
|
57
|
+
defaultPolicy: "deny",
|
|
58
|
+
allowlist_domains: ["trailing.loop"],
|
|
59
|
+
};
|
|
60
|
+
assert.strictEqual(engine.evaluateDomain("trailing.loop.", config), "ALLOW");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("FirewallEngine - IP Evaluation", () => {
|
|
64
|
+
const engine = new FirewallEngine();
|
|
65
|
+
it("blocks malformed IPs universally", () => {
|
|
66
|
+
const config = { defaultPolicy: "allow" };
|
|
67
|
+
assert.strictEqual(engine.evaluateIp("not.an.ip", config), "DENY");
|
|
68
|
+
assert.strictEqual(engine.evaluateIp("999.999.999.999", config), "DENY");
|
|
69
|
+
});
|
|
70
|
+
it("blocks explicitly blocklisted IPs", () => {
|
|
71
|
+
const config = {
|
|
72
|
+
defaultPolicy: "allow",
|
|
73
|
+
blocklist_ips: ["192.168.1.100"],
|
|
74
|
+
};
|
|
75
|
+
assert.strictEqual(engine.evaluateIp("192.168.1.100", config), "DENY");
|
|
76
|
+
});
|
|
77
|
+
it("blocks IPs matching blocklist CIDR ranges", () => {
|
|
78
|
+
const config = {
|
|
79
|
+
defaultPolicy: "allow",
|
|
80
|
+
blocklist_ranges: ["10.0.0.0/8"],
|
|
81
|
+
};
|
|
82
|
+
assert.strictEqual(engine.evaluateIp("10.5.5.5", config), "DENY");
|
|
83
|
+
assert.strictEqual(engine.evaluateIp("11.0.0.1", config), "ALLOW");
|
|
84
|
+
});
|
|
85
|
+
it("allows IPs matching allowlist CIDR ranges overriding default deny", () => {
|
|
86
|
+
const config = {
|
|
87
|
+
defaultPolicy: "deny",
|
|
88
|
+
allowlist_ranges: ["172.16.0.0/12"],
|
|
89
|
+
};
|
|
90
|
+
assert.strictEqual(engine.evaluateIp("172.20.5.1", config), "ALLOW");
|
|
91
|
+
assert.strictEqual(engine.evaluateIp("192.168.1.1", config), "DENY");
|
|
92
|
+
});
|
|
93
|
+
it("prioritizes IP blocklist over IP allowlist", () => {
|
|
94
|
+
const config = {
|
|
95
|
+
defaultPolicy: "deny",
|
|
96
|
+
allowlist_ips: ["1.1.1.1"],
|
|
97
|
+
blocklist_ips: ["1.1.1.1"],
|
|
98
|
+
};
|
|
99
|
+
assert.strictEqual(engine.evaluateIp("1.1.1.1", config), "DENY");
|
|
100
|
+
});
|
|
101
|
+
it("safely ignores malformed CIDR ranges without crashing", () => {
|
|
102
|
+
const config = {
|
|
103
|
+
defaultPolicy: "deny",
|
|
104
|
+
allowlist_ranges: ["invalid/cidr/string"],
|
|
105
|
+
allowlist_ips: ["8.8.8.8"]
|
|
106
|
+
};
|
|
107
|
+
assert.strictEqual(engine.evaluateIp("8.8.8.8", config), "ALLOW");
|
|
108
|
+
assert.strictEqual(engine.evaluateIp("1.1.1.1", config), "DENY");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe("FirewallEngine - Outbound SSRF Protection", () => {
|
|
112
|
+
const engine = new FirewallEngine();
|
|
113
|
+
it("blocks IPv4 loopback addresses natively", () => {
|
|
114
|
+
assert.strictEqual(engine.isRestrictedOutbound("127.0.0.1"), true);
|
|
115
|
+
assert.strictEqual(engine.isRestrictedOutbound("127.255.255.254"), true);
|
|
116
|
+
});
|
|
117
|
+
it("blocks IPv4 RFC1918 private network addresses natively", () => {
|
|
118
|
+
assert.strictEqual(engine.isRestrictedOutbound("10.0.0.1"), true);
|
|
119
|
+
assert.strictEqual(engine.isRestrictedOutbound("172.16.0.1"), true);
|
|
120
|
+
assert.strictEqual(engine.isRestrictedOutbound("172.31.255.255"), true);
|
|
121
|
+
assert.strictEqual(engine.isRestrictedOutbound("192.168.1.1"), true);
|
|
122
|
+
});
|
|
123
|
+
it("blocks IPv4 link-local and broadcast addresses natively", () => {
|
|
124
|
+
assert.strictEqual(engine.isRestrictedOutbound("169.254.169.254"), true);
|
|
125
|
+
assert.strictEqual(engine.isRestrictedOutbound("0.0.0.0"), true);
|
|
126
|
+
assert.strictEqual(engine.isRestrictedOutbound("224.0.0.1"), true);
|
|
127
|
+
assert.strictEqual(engine.isRestrictedOutbound("255.255.255.255"), true);
|
|
128
|
+
});
|
|
129
|
+
it("blocks IPv4 Carrier-Grade NAT addresses natively", () => {
|
|
130
|
+
assert.strictEqual(engine.isRestrictedOutbound("100.64.0.1"), true);
|
|
131
|
+
assert.strictEqual(engine.isRestrictedOutbound("100.127.255.254"), true);
|
|
132
|
+
});
|
|
133
|
+
it("allows standard public IPv4 addresses", () => {
|
|
134
|
+
assert.strictEqual(engine.isRestrictedOutbound("8.8.8.8"), false);
|
|
135
|
+
assert.strictEqual(engine.isRestrictedOutbound("1.1.1.1"), false);
|
|
136
|
+
assert.strictEqual(engine.isRestrictedOutbound("104.21.5.1"), false);
|
|
137
|
+
});
|
|
138
|
+
it("blocks IPv6 loopback and unspecified addresses natively", () => {
|
|
139
|
+
assert.strictEqual(engine.isRestrictedOutbound("::1"), true);
|
|
140
|
+
assert.strictEqual(engine.isRestrictedOutbound("::"), true);
|
|
141
|
+
});
|
|
142
|
+
it("blocks IPv6 link-local and unique local addresses natively", () => {
|
|
143
|
+
assert.strictEqual(engine.isRestrictedOutbound("fe80::1"), true);
|
|
144
|
+
assert.strictEqual(engine.isRestrictedOutbound("fc00::1"), true);
|
|
145
|
+
assert.strictEqual(engine.isRestrictedOutbound("fd00::1"), true);
|
|
146
|
+
});
|
|
147
|
+
it("blocks IPv4-mapped IPv6 addresses for restricted ranges", () => {
|
|
148
|
+
assert.strictEqual(engine.isRestrictedOutbound("::ffff:127.0.0.1"), true);
|
|
149
|
+
assert.strictEqual(engine.isRestrictedOutbound("::ffff:169.254.169.254"), true);
|
|
150
|
+
});
|
|
151
|
+
it("allows configured overrides for restricted IPs via allowlist", () => {
|
|
152
|
+
const config = {
|
|
153
|
+
defaultPolicy: "deny",
|
|
154
|
+
allowlist_ips: ["127.0.0.1"],
|
|
155
|
+
};
|
|
156
|
+
assert.strictEqual(engine.evaluateOutbound("127.0.0.1", config), "ALLOW");
|
|
157
|
+
});
|
|
158
|
+
it("allows configured overrides for restricted IPs via CIDR", () => {
|
|
159
|
+
const config = {
|
|
160
|
+
defaultPolicy: "deny",
|
|
161
|
+
allowlist_ranges: ["10.0.0.0/8"],
|
|
162
|
+
};
|
|
163
|
+
assert.strictEqual(engine.evaluateOutbound("10.5.0.1", config), "ALLOW");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "assert";
|
|
2
|
+
import assert from "node:assert";
|
|
3
3
|
import * as http from "http";
|
|
4
4
|
describe("HTTP Body Forwarding Integration", () => {
|
|
5
5
|
let upstreamServer;
|
|
@@ -131,6 +131,9 @@ describe("HTTP Body Forwarding Integration", () => {
|
|
|
131
131
|
try {
|
|
132
132
|
const config = {
|
|
133
133
|
port: 53,
|
|
134
|
+
firewall: {
|
|
135
|
+
allowlist_ips: ["127.0.0.1"]
|
|
136
|
+
},
|
|
134
137
|
hosts: {
|
|
135
138
|
"app.loop": {
|
|
136
139
|
records: [{ type: "A", address: "127.0.0.1" }],
|
|
@@ -170,6 +173,9 @@ describe("HTTP Body Forwarding Integration", () => {
|
|
|
170
173
|
try {
|
|
171
174
|
const config = {
|
|
172
175
|
port: 53,
|
|
176
|
+
firewall: {
|
|
177
|
+
allowlist_ips: ["127.0.0.1"]
|
|
178
|
+
},
|
|
173
179
|
hosts: {
|
|
174
180
|
"app.loop": {
|
|
175
181
|
records: [{ type: "A", address: "127.0.0.1" }],
|
|
@@ -209,6 +215,9 @@ describe("HTTP Body Forwarding Integration", () => {
|
|
|
209
215
|
try {
|
|
210
216
|
const config = {
|
|
211
217
|
port: 53,
|
|
218
|
+
firewall: {
|
|
219
|
+
allowlist_ips: ["127.0.0.1"]
|
|
220
|
+
},
|
|
212
221
|
hosts: {
|
|
213
222
|
"app.loop": {
|
|
214
223
|
records: [{ type: "A", address: "127.0.0.1" }],
|
|
@@ -246,6 +255,9 @@ describe("HTTP Body Forwarding Integration", () => {
|
|
|
246
255
|
try {
|
|
247
256
|
const config = {
|
|
248
257
|
port: 53,
|
|
258
|
+
firewall: {
|
|
259
|
+
allowlist_ips: ["127.0.0.1"]
|
|
260
|
+
},
|
|
249
261
|
hosts: {
|
|
250
262
|
"app.loop": {
|
|
251
263
|
records: [{ type: "A", address: "127.0.0.1" }],
|
|
@@ -285,6 +297,9 @@ describe("HTTP Body Forwarding Integration", () => {
|
|
|
285
297
|
try {
|
|
286
298
|
const config = {
|
|
287
299
|
port: 53,
|
|
300
|
+
firewall: {
|
|
301
|
+
allowlist_ips: ["127.0.0.1"]
|
|
302
|
+
},
|
|
288
303
|
hosts: {
|
|
289
304
|
"app.loop": {
|
|
290
305
|
records: [{ type: "A", address: "127.0.0.1" }],
|
package/dist/http-handler.d.ts
CHANGED
|
@@ -8,11 +8,13 @@ export declare class HttpHandler {
|
|
|
8
8
|
private server;
|
|
9
9
|
private circuitBreakers;
|
|
10
10
|
private activeConnections;
|
|
11
|
+
private idleTimeoutMs;
|
|
11
12
|
constructor(dnsServer: DevDnsServer, config: ServerConfig, port?: number);
|
|
12
13
|
private getCircuitBreaker;
|
|
13
14
|
private findWildcardHost;
|
|
14
|
-
private injectCustomHeaders;
|
|
15
15
|
private handleConnect;
|
|
16
|
+
private doHttpProxy;
|
|
17
|
+
private readRequestBodySafe;
|
|
16
18
|
private handleForwardProxy;
|
|
17
19
|
private handleRequest;
|
|
18
20
|
start(): Promise<void>;
|