@sonoma-security/mcp-gateway 0.1.4 → 0.1.5

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.
Files changed (62) hide show
  1. package/README.md +104 -45
  2. package/dist/__tests__/config.test.js +28 -0
  3. package/dist/__tests__/config.test.js.map +1 -1
  4. package/dist/__tests__/ssrf-protection.test.d.ts +2 -0
  5. package/dist/__tests__/ssrf-protection.test.d.ts.map +1 -0
  6. package/dist/__tests__/ssrf-protection.test.js +389 -0
  7. package/dist/__tests__/ssrf-protection.test.js.map +1 -0
  8. package/dist/auth/client.d.ts +2 -0
  9. package/dist/auth/client.d.ts.map +1 -1
  10. package/dist/auth/client.js +17 -15
  11. package/dist/auth/client.js.map +1 -1
  12. package/dist/auth/crypto.d.ts +23 -0
  13. package/dist/auth/crypto.d.ts.map +1 -0
  14. package/dist/auth/crypto.js +78 -0
  15. package/dist/auth/crypto.js.map +1 -0
  16. package/dist/auth/index.d.ts +4 -1
  17. package/dist/auth/index.d.ts.map +1 -1
  18. package/dist/auth/index.js +4 -1
  19. package/dist/auth/index.js.map +1 -1
  20. package/dist/auth/server.d.ts +2 -0
  21. package/dist/auth/server.d.ts.map +1 -1
  22. package/dist/auth/server.js +337 -59
  23. package/dist/auth/server.js.map +1 -1
  24. package/dist/auth/storage.d.ts.map +1 -1
  25. package/dist/auth/storage.js +2 -72
  26. package/dist/auth/storage.js.map +1 -1
  27. package/dist/auth/upstream-oauth-provider.d.ts +41 -0
  28. package/dist/auth/upstream-oauth-provider.d.ts.map +1 -0
  29. package/dist/auth/upstream-oauth-provider.js +88 -0
  30. package/dist/auth/upstream-oauth-provider.js.map +1 -0
  31. package/dist/auth/upstream-oauth.d.ts +31 -0
  32. package/dist/auth/upstream-oauth.d.ts.map +1 -0
  33. package/dist/auth/upstream-oauth.js +79 -0
  34. package/dist/auth/upstream-oauth.js.map +1 -0
  35. package/dist/auth/upstream-token-store.d.ts +27 -0
  36. package/dist/auth/upstream-token-store.d.ts.map +1 -0
  37. package/dist/auth/upstream-token-store.js +103 -0
  38. package/dist/auth/upstream-token-store.js.map +1 -0
  39. package/dist/cli.js +83 -63
  40. package/dist/cli.js.map +1 -1
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/config.js +94 -9
  43. package/dist/config.js.map +1 -1
  44. package/dist/gateway.d.ts +23 -1
  45. package/dist/gateway.d.ts.map +1 -1
  46. package/dist/gateway.js +224 -35
  47. package/dist/gateway.js.map +1 -1
  48. package/dist/pattern-matcher.d.ts +47 -0
  49. package/dist/pattern-matcher.d.ts.map +1 -0
  50. package/dist/pattern-matcher.js +98 -0
  51. package/dist/pattern-matcher.js.map +1 -0
  52. package/dist/sonoma-client.d.ts +21 -5
  53. package/dist/sonoma-client.d.ts.map +1 -1
  54. package/dist/sonoma-client.js +42 -2
  55. package/dist/sonoma-client.js.map +1 -1
  56. package/dist/ssrf-protection.d.ts +59 -0
  57. package/dist/ssrf-protection.d.ts.map +1 -0
  58. package/dist/ssrf-protection.js +253 -0
  59. package/dist/ssrf-protection.js.map +1 -0
  60. package/dist/types.d.ts +6 -2
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +2 -2
package/README.md CHANGED
@@ -1,82 +1,141 @@
1
- # @sonoma/mcp-gateway
1
+ # @sonoma-security/mcp-gateway
2
2
 
3
- Local MCP proxy for tool-level visibility. Intercepts stdio MCP servers, aggregates tools, forwards calls.
3
+ Local MCP gateway for tool-level visibility and policy enforcement. Intercepts MCP tool calls, reports telemetry to Sonoma, and enforces allowlist/blocklist policies.
4
4
 
5
- ## Usage
5
+ ## Quick Start
6
6
 
7
- ```bash
8
- # With config file
9
- bun run src/cli.ts --config config.json
10
-
11
- # Auto-detect Claude Desktop config
12
- bun run src/cli.ts --auto --debug
13
- ```
14
-
15
- ## Config Format
7
+ Add to your MCP client config (`~/.cursor/mcp.json`, Claude Desktop, etc.):
16
8
 
17
9
  ```json
18
10
  {
19
- "servers": [
20
- { "name": "fs", "command": "npx", "args": ["-y", "@anthropic-ai/mcp-server-filesystem", "/tmp"] }
21
- ]
11
+ "mcpServers": {
12
+ "sonoma": {
13
+ "command": "npx",
14
+ "args": ["@sonoma-security/mcp-gateway@latest"],
15
+ "servers": {
16
+ "filesystem": {
17
+ "command": "npx",
18
+ "args": ["@modelcontextprotocol/server-filesystem", "/tmp"]
19
+ }
20
+ }
21
+ }
22
+ }
22
23
  }
23
24
  ```
24
25
 
25
- Also accepts Claude Desktop `mcpServers` format.
26
+ On first run, the gateway prompts for OAuth login. That's it - tool calls are now visible in your Sonoma dashboard.
26
27
 
27
- ## Add to MCP Client
28
+ ## How It Works
29
+
30
+ ```
31
+ AI Client (stdio) -> Sonoma Gateway -> [Upstream MCP Servers]
32
+ |
33
+ +-> Telemetry to Sonoma
34
+ +-> Policy enforcement
35
+ ```
36
+
37
+ The gateway:
38
+ 1. Spawns your upstream MCP servers
39
+ 2. Aggregates tools (namespaced as `serverName__toolName`)
40
+ 3. Forwards tool calls to the appropriate server
41
+ 4. Reports telemetry (tool name, duration, status)
42
+ 5. Blocks tools per your org's allowlist/blocklist
43
+
44
+ ## Auto-Detection
45
+
46
+ By default (no arguments), the gateway auto-detects config from:
47
+ - `~/.claude.json` (Claude Code)
48
+ - `~/.cursor/mcp.json` (Cursor)
49
+ - `~/Library/Application Support/Claude/claude_desktop_config.json` (Claude Desktop)
50
+ - `~/.codeium/windsurf/mcp_config.json` (Windsurf)
51
+
52
+ ## Non-Standard Config Location
53
+
54
+ If your config is elsewhere, pass `--mcp-json-path`:
28
55
 
29
- **Cursor** (`~/.cursor/mcp.json`):
30
56
  ```json
31
57
  {
32
58
  "mcpServers": {
33
59
  "sonoma": {
34
- "command": "bun",
35
- "args": ["run", "/path/to/packages/mcp-gateway/src/cli.ts", "--config", "/path/to/config.json"]
60
+ "command": "npx",
61
+ "args": ["@sonoma-security/mcp-gateway", "--mcp-json-path", "/custom/path/mcp.json"],
62
+ "servers": { ... }
36
63
  }
37
64
  }
38
65
  }
39
66
  ```
40
67
 
41
- **Claude Code**:
68
+ ## Auth Modes
69
+
70
+ ### User ID (default)
71
+ Opens browser for OAuth on first run. Tool calls attributed to logged-in user.
72
+
73
+ ### Org Key
74
+ Device-level telemetry via API key. No login required.
75
+
76
+ ```json
77
+ "env": {
78
+ "SONOMA_API_KEY": "org_xxx_yyy",
79
+ "SONOMA_GATEWAY_AUTH_MODE": "org_key"
80
+ }
81
+ ```
82
+
83
+ ## CLI
84
+
42
85
  ```bash
43
- claude mcp add sonoma -- bun run /path/to/packages/mcp-gateway/src/cli.ts --config /path/to/config.json
86
+ npx @sonoma-security/mcp-gateway --help
87
+
88
+ # Auth commands
89
+ npx @sonoma-security/mcp-gateway --login # OAuth login
90
+ npx @sonoma-security/mcp-gateway --logout # Clear credentials
91
+ npx @sonoma-security/mcp-gateway --status # Show auth status
92
+
93
+ # Debug mode
94
+ npx @sonoma-security/mcp-gateway --debug
44
95
  ```
45
96
 
97
+ ## Environment Variables
98
+
99
+ | Variable | Description |
100
+ |----------|-------------|
101
+ | `SONOMA_ENDPOINT` | Sonoma API URL (default: `https://app.sonoma.dev`) |
102
+ | `SONOMA_API_KEY` | Org API key (for org_key mode) |
103
+ | `SONOMA_GATEWAY_AUTH_MODE` | `user_id` or `org_key` |
104
+
105
+ ## Limitations
106
+
107
+ - **Stdio servers only** - HTTP MCP servers not supported
108
+ - **Nested config** - Servers must be nested under the gateway entry
109
+
46
110
  ## Development
47
111
 
48
112
  ```bash
49
- # Run tests (vitest)
113
+ # Run tests
50
114
  bun run test
51
115
 
52
- # Watch mode
53
- bun run test -- --watch
54
-
55
116
  # Typecheck
56
117
  bun run typecheck
118
+
119
+ # Test with MCP Inspector
120
+ npx @modelcontextprotocol/inspector bun run src/cli.ts --config ./test-config.json --debug
57
121
  ```
58
122
 
59
- ## Manual JSON-RPC Testing
123
+ ## Releasing
60
124
 
61
- ```bash
62
- cat << 'EOF' | bun run src/cli.ts --config tests/fixtures/echo-server.json
63
- {"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}
64
- {"jsonrpc":"2.0","method":"tools/list","id":2}
65
- EOF
66
- ```
125
+ Publishing is automated via GitHub Actions with npm trusted publishing (OIDC).
67
126
 
68
- ## Architecture
127
+ **To release a new version:**
69
128
 
70
- ```
71
- AI Client (stdio) Gateway [Upstream MCP Servers]
72
-
73
- Tools namespaced as serverName__toolName
74
- ```
129
+ 1. Update version in `package.json`
130
+ 2. Commit: `git commit -am "chore(mcp-gateway): bump to x.y.z"`
131
+ 3. Tag: `git tag mcp-gateway-vX.Y.Z`
132
+ 4. Push both: `git push origin staging --tags`
133
+
134
+ CI automatically builds, tests, and publishes to npm. No tokens required.
135
+
136
+ **What's automated:** build, test, typecheck, npm publish with provenance
137
+ **What's manual:** version bump, git tag, push
75
138
 
76
- ## Status
139
+ ## License
77
140
 
78
- - [x] Core proxy (tools/list, tools/call forwarding)
79
- - [x] Config loader (native + Claude Desktop format)
80
- - [x] CLI
81
- - [ ] Telemetry reporter
82
- - [ ] Policy enforcement
141
+ MIT
@@ -171,4 +171,32 @@ describe("loadFromParentConfig", () => {
171
171
  expect(() => loadFromParentConfig(configPath)).toThrow("No mcpServers found");
172
172
  });
173
173
  });
174
+ describe("config path validation", () => {
175
+ it("rejects non-JSON file extensions", () => {
176
+ expect(() => loadConfig("/some/path/config.txt")).toThrow("Config files must have .json extension");
177
+ expect(() => loadConfig("/etc/passwd")).toThrow("Config files must have .json extension");
178
+ expect(() => loadConfig("/some/path/config.yaml")).toThrow("Config files must have .json extension");
179
+ });
180
+ it("rejects paths outside allowed directories", () => {
181
+ // This path won't exist, but validation happens before existence check
182
+ // for files that do exist but are outside allowed dirs
183
+ // The .json extension check happens first, so this should pass extension check
184
+ // but fail on the location check IF the file existed
185
+ expect(() => loadConfig("/nonexistent/path/config.json")).toThrow("Config file not found");
186
+ });
187
+ it("accepts paths in temp directory", () => {
188
+ const testDir = join(tmpdir(), `mcp-gateway-security-test-${Date.now()}`);
189
+ mkdirSync(testDir, { recursive: true });
190
+ try {
191
+ const configPath = join(testDir, "valid.json");
192
+ writeFileSync(configPath, JSON.stringify({ servers: [] }));
193
+ // Should not throw - temp dir is allowed
194
+ const config = loadConfig(configPath);
195
+ expect(config.servers).toEqual([]);
196
+ }
197
+ finally {
198
+ rmSync(testDir, { recursive: true, force: true });
199
+ }
200
+ });
201
+ });
174
202
  //# sourceMappingURL=config.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ssrf-protection.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf-protection.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ssrf-protection.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Unit tests for SSRF protection module
3
+ *
4
+ * Tests all exported functions:
5
+ * - ipToNum: IPv4 dotted-quad to 32-bit number conversion
6
+ * - isBlockedIP: IP range blocking (RFC 1918, link-local, etc.)
7
+ * - validateUrl: URL validation with DNS resolution and IP checks
8
+ * - safeFetch: Safe fetch with DNS pinning and redirect validation
9
+ */
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
11
+ import { ipToNum, isBlockedIP, validateUrl, safeFetch } from "../ssrf-protection.js";
12
+ import dns from "node:dns/promises";
13
+ describe("ipToNum", () => {
14
+ it("converts 0.0.0.0 to 0", () => {
15
+ expect(ipToNum("0.0.0.0")).toBe(0);
16
+ });
17
+ it("converts 255.255.255.255 to 4294967295", () => {
18
+ expect(ipToNum("255.255.255.255")).toBe(4294967295);
19
+ });
20
+ it("converts 192.168.1.1 correctly (tests unsigned shift)", () => {
21
+ // 192.168.1.1 = (192 << 24) | (168 << 16) | (1 << 8) | 1
22
+ // Without >>> 0, this would be negative due to sign bit
23
+ expect(ipToNum("192.168.1.1")).toBe(3232235777);
24
+ });
25
+ it("converts 10.0.0.1", () => {
26
+ expect(ipToNum("10.0.0.1")).toBe(167772161);
27
+ });
28
+ it("converts 172.16.0.1", () => {
29
+ expect(ipToNum("172.16.0.1")).toBe(2886729729);
30
+ });
31
+ it("converts 8.8.8.8 (public IP)", () => {
32
+ expect(ipToNum("8.8.8.8")).toBe(134744072);
33
+ });
34
+ });
35
+ describe("isBlockedIP", () => {
36
+ describe("RFC 1918 private ranges", () => {
37
+ it("blocks 10.0.0.0/8", () => {
38
+ expect(isBlockedIP("10.0.0.1")).toBe(true);
39
+ expect(isBlockedIP("10.128.0.1")).toBe(true);
40
+ expect(isBlockedIP("10.255.255.254")).toBe(true);
41
+ });
42
+ it("blocks 172.16.0.0/12", () => {
43
+ expect(isBlockedIP("172.16.0.1")).toBe(true);
44
+ expect(isBlockedIP("172.20.0.1")).toBe(true);
45
+ expect(isBlockedIP("172.31.255.254")).toBe(true);
46
+ });
47
+ it("blocks 192.168.0.0/16", () => {
48
+ expect(isBlockedIP("192.168.0.1")).toBe(true);
49
+ expect(isBlockedIP("192.168.1.1")).toBe(true);
50
+ expect(isBlockedIP("192.168.255.254")).toBe(true);
51
+ });
52
+ });
53
+ describe("link-local and metadata endpoints", () => {
54
+ it("blocks 169.254.0.0/16", () => {
55
+ expect(isBlockedIP("169.254.0.1")).toBe(true);
56
+ expect(isBlockedIP("169.254.169.254")).toBe(true); // AWS/GCP/Azure metadata
57
+ expect(isBlockedIP("169.254.255.254")).toBe(true);
58
+ });
59
+ });
60
+ describe('"this" network', () => {
61
+ it("blocks 0.0.0.0/8", () => {
62
+ expect(isBlockedIP("0.0.0.1")).toBe(true);
63
+ expect(isBlockedIP("0.128.0.1")).toBe(true);
64
+ expect(isBlockedIP("0.255.255.254")).toBe(true);
65
+ });
66
+ });
67
+ describe("loopback addresses (intentionally allowed)", () => {
68
+ it("allows 127.0.0.1", () => {
69
+ expect(isBlockedIP("127.0.0.1")).toBe(false);
70
+ });
71
+ it("allows 127.0.0.2", () => {
72
+ expect(isBlockedIP("127.0.0.2")).toBe(false);
73
+ });
74
+ it("allows 127.255.255.254", () => {
75
+ expect(isBlockedIP("127.255.255.254")).toBe(false);
76
+ });
77
+ });
78
+ describe("public IPv4 addresses", () => {
79
+ it("allows 8.8.8.8 (Google DNS)", () => {
80
+ expect(isBlockedIP("8.8.8.8")).toBe(false);
81
+ });
82
+ it("allows 1.1.1.1 (Cloudflare DNS)", () => {
83
+ expect(isBlockedIP("1.1.1.1")).toBe(false);
84
+ });
85
+ it("allows 151.101.1.140 (Fastly)", () => {
86
+ expect(isBlockedIP("151.101.1.140")).toBe(false);
87
+ });
88
+ });
89
+ describe("IPv4-mapped IPv6 addresses", () => {
90
+ it("blocks ::ffff:10.0.0.1 (private)", () => {
91
+ expect(isBlockedIP("::ffff:10.0.0.1")).toBe(true);
92
+ });
93
+ it("blocks ::ffff:169.254.169.254 (metadata)", () => {
94
+ expect(isBlockedIP("::ffff:169.254.169.254")).toBe(true);
95
+ });
96
+ it("blocks ::FFFF:192.168.1.1 (case-insensitive)", () => {
97
+ expect(isBlockedIP("::FFFF:192.168.1.1")).toBe(true);
98
+ });
99
+ it("allows ::ffff:127.0.0.1 (loopback - intentional)", () => {
100
+ expect(isBlockedIP("::ffff:127.0.0.1")).toBe(false);
101
+ });
102
+ it("allows ::ffff:8.8.8.8 (public)", () => {
103
+ expect(isBlockedIP("::ffff:8.8.8.8")).toBe(false);
104
+ });
105
+ });
106
+ describe("IPv6 link-local addresses", () => {
107
+ it("blocks fe80::1", () => {
108
+ expect(isBlockedIP("fe80::1")).toBe(true);
109
+ });
110
+ it("blocks fe80::abcd:ef01:2345:6789", () => {
111
+ expect(isBlockedIP("fe80::abcd:ef01:2345:6789")).toBe(true);
112
+ });
113
+ it("blocks febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff (edge of fe80::/10)", () => {
114
+ expect(isBlockedIP("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")).toBe(true);
115
+ });
116
+ });
117
+ describe("IPv6 unique-local addresses", () => {
118
+ it("blocks fc00::1 (ULA)", () => {
119
+ expect(isBlockedIP("fc00::1")).toBe(true);
120
+ });
121
+ it("blocks fd00::1 (ULA)", () => {
122
+ expect(isBlockedIP("fd00::1")).toBe(true);
123
+ });
124
+ it("blocks fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff (edge of fc00::/7)", () => {
125
+ expect(isBlockedIP("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")).toBe(true);
126
+ });
127
+ });
128
+ describe("IPv6 public addresses", () => {
129
+ it("allows 2001:db8::1 (documentation prefix, but not in block list)", () => {
130
+ expect(isBlockedIP("2001:db8::1")).toBe(false);
131
+ });
132
+ it("allows 2606:4700:4700::1111 (Cloudflare DNS)", () => {
133
+ expect(isBlockedIP("2606:4700:4700::1111")).toBe(false);
134
+ });
135
+ it("allows 2001:4860:4860::8888 (Google DNS)", () => {
136
+ expect(isBlockedIP("2001:4860:4860::8888")).toBe(false);
137
+ });
138
+ });
139
+ describe("IPv6 loopback (intentionally allowed)", () => {
140
+ it("allows ::1", () => {
141
+ expect(isBlockedIP("::1")).toBe(false);
142
+ });
143
+ });
144
+ });
145
+ describe("validateUrl", () => {
146
+ beforeEach(() => {
147
+ // Mock dns.lookup for most tests
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ vi.spyOn(dns, "lookup").mockImplementation((async (hostname) => {
150
+ // Default: return a public IP
151
+ if (hostname === "example.com") {
152
+ return [{ address: "93.184.216.34", family: 4 }];
153
+ }
154
+ if (hostname === "private.local") {
155
+ return [{ address: "10.0.0.1", family: 4 }];
156
+ }
157
+ if (hostname === "metadata.local") {
158
+ return [{ address: "169.254.169.254", family: 4 }];
159
+ }
160
+ if (hostname === "multi-ip.local") {
161
+ return [
162
+ { address: "8.8.8.8", family: 4 },
163
+ { address: "10.0.0.1", family: 4 }, // One blocked IP
164
+ ];
165
+ }
166
+ if (hostname === "notfound.local") {
167
+ const error = new Error("getaddrinfo ENOTFOUND");
168
+ error.code = "ENOTFOUND";
169
+ throw error;
170
+ }
171
+ // Default fallback
172
+ return [{ address: "1.1.1.1", family: 4 }];
173
+ }));
174
+ });
175
+ afterEach(() => {
176
+ vi.restoreAllMocks();
177
+ });
178
+ describe("protocol validation", () => {
179
+ it("rejects ftp:// protocol", async () => {
180
+ await expect(validateUrl("ftp://example.com")).rejects.toThrow("Protocol ftp: is not allowed");
181
+ });
182
+ it("rejects file:// protocol", async () => {
183
+ await expect(validateUrl("file:///etc/passwd")).rejects.toThrow("Protocol file: is not allowed");
184
+ });
185
+ it("rejects javascript: protocol", async () => {
186
+ await expect(validateUrl("javascript:alert(1)")).rejects.toThrow("Protocol javascript: is not allowed");
187
+ });
188
+ it("allows http:// protocol", async () => {
189
+ const result = await validateUrl("http://example.com");
190
+ expect(result.url.protocol).toBe("http:");
191
+ });
192
+ it("allows https:// protocol", async () => {
193
+ const result = await validateUrl("https://example.com");
194
+ expect(result.url.protocol).toBe("https:");
195
+ });
196
+ });
197
+ describe("blocked hostname detection", () => {
198
+ it("blocks metadata.google.internal", async () => {
199
+ await expect(validateUrl("http://metadata.google.internal")).rejects.toThrow("Access to metadata.google.internal is blocked for security reasons");
200
+ });
201
+ it("blocks metadata (GCP short name)", async () => {
202
+ await expect(validateUrl("http://metadata/")).rejects.toThrow("Access to metadata is blocked for security reasons");
203
+ });
204
+ it("blocks metadata.goog", async () => {
205
+ await expect(validateUrl("http://metadata.goog")).rejects.toThrow("Access to metadata.goog is blocked for security reasons");
206
+ });
207
+ it("blocks 169.254.169.254 as hostname", async () => {
208
+ // When IP is in hostname, it first checks BLOCKED_HOSTNAMES
209
+ await expect(validateUrl("http://169.254.169.254")).rejects.toThrow("Access to 169.254.169.254 is blocked for security reasons");
210
+ });
211
+ it("blocks fd00:ec2::254 (AWS IPv6 metadata) via BLOCKED_HOSTNAMES", async () => {
212
+ // The BLOCKED_HOSTNAMES check uses plain fd00:ec2::254 (without brackets).
213
+ // When used in a URL, IPv6 addresses require brackets [fd00:ec2::254],
214
+ // but url.hostname in Bun returns them WITH brackets, so net.isIP returns 0,
215
+ // triggering DNS resolution. We test the hostname check directly by mocking
216
+ // DNS to return the blocked address.
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ vi.spyOn(dns, "lookup").mockImplementation((async (hostname) => {
219
+ if (hostname === "[fd00:ec2::254]") {
220
+ return [{ address: "fd00:ec2::254", family: 6 }];
221
+ }
222
+ if (hostname === "example.com") {
223
+ return [{ address: "93.184.216.34", family: 4 }];
224
+ }
225
+ return [{ address: "1.1.1.1", family: 4 }];
226
+ }));
227
+ // Should be blocked via DNS resolution check
228
+ await expect(validateUrl("http://[fd00:ec2::254]")).rejects.toThrow("resolves to private IP fd00:ec2::254");
229
+ });
230
+ it("allows example.com", async () => {
231
+ const result = await validateUrl("http://example.com");
232
+ expect(result.url.hostname).toBe("example.com");
233
+ });
234
+ });
235
+ describe("IP address validation in URL", () => {
236
+ it("blocks 10.0.0.1 (private)", async () => {
237
+ await expect(validateUrl("http://10.0.0.1")).rejects.toThrow("Access to private IP 10.0.0.1 is blocked for security reasons");
238
+ });
239
+ it("blocks 192.168.1.1 (private)", async () => {
240
+ await expect(validateUrl("http://192.168.1.1")).rejects.toThrow("Access to private IP 192.168.1.1 is blocked for security reasons");
241
+ });
242
+ it("blocks IPv6 fc00::1 (ULA) via DNS resolution", async () => {
243
+ // Set up mock to return a blocked IPv6 address
244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
+ vi.spyOn(dns, "lookup").mockImplementation((async (hostname) => {
246
+ if (hostname === "[fc00::1]") {
247
+ // Bun keeps brackets in url.hostname for IPv6
248
+ return [{ address: "fc00::1", family: 6 }];
249
+ }
250
+ if (hostname === "example.com") {
251
+ return [{ address: "93.184.216.34", family: 4 }];
252
+ }
253
+ return [{ address: "1.1.1.1", family: 4 }];
254
+ }));
255
+ // URL with brackets triggers DNS lookup in Bun (net.isIP("[fc00::1]") === 0)
256
+ await expect(validateUrl("http://[fc00::1]")).rejects.toThrow("resolves to private IP fc00::1");
257
+ });
258
+ it("allows 8.8.8.8 (public)", async () => {
259
+ const result = await validateUrl("http://8.8.8.8");
260
+ expect(result.url.hostname).toBe("8.8.8.8");
261
+ expect(result.resolvedAddress).toBeNull();
262
+ });
263
+ it("allows localhost (intentional for CLI)", async () => {
264
+ // localhost is not an IP, so DNS resolution happens
265
+ const result = await validateUrl("http://localhost:3000");
266
+ expect(result.url.hostname).toBe("localhost");
267
+ expect(result.resolvedAddress).toBe("1.1.1.1"); // From our mock
268
+ });
269
+ it("allows 127.0.0.1 (intentional for CLI)", async () => {
270
+ const result = await validateUrl("http://127.0.0.1:8080");
271
+ expect(result.url.hostname).toBe("127.0.0.1");
272
+ expect(result.resolvedAddress).toBeNull();
273
+ });
274
+ it("allows IPv6 ::1 (loopback) via DNS resolution", async () => {
275
+ // Set up mock to return loopback IPv6
276
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
277
+ vi.spyOn(dns, "lookup").mockImplementation((async (hostname) => {
278
+ if (hostname === "[::1]") {
279
+ // Bun keeps brackets in url.hostname for IPv6
280
+ return [{ address: "::1", family: 6 }];
281
+ }
282
+ if (hostname === "example.com") {
283
+ return [{ address: "93.184.216.34", family: 4 }];
284
+ }
285
+ return [{ address: "1.1.1.1", family: 4 }];
286
+ }));
287
+ const result = await validateUrl("http://[::1]:9000");
288
+ // Bun keeps brackets in hostname for IPv6
289
+ expect(result.url.hostname).toBe("[::1]");
290
+ expect(result.resolvedAddress).toBe("::1");
291
+ });
292
+ });
293
+ describe("DNS resolution", () => {
294
+ it("returns resolved address for hostname-based URLs", async () => {
295
+ const result = await validateUrl("http://example.com");
296
+ expect(result.resolvedAddress).toBe("93.184.216.34");
297
+ });
298
+ it("returns null resolvedAddress for IP-based URLs", async () => {
299
+ const result = await validateUrl("http://8.8.8.8");
300
+ expect(result.resolvedAddress).toBeNull();
301
+ });
302
+ it("blocks hostname that resolves to private IP", async () => {
303
+ await expect(validateUrl("http://private.local")).rejects.toThrow("Hostname private.local resolves to private IP 10.0.0.1");
304
+ });
305
+ it("blocks hostname that resolves to metadata endpoint", async () => {
306
+ await expect(validateUrl("http://metadata.local")).rejects.toThrow("Hostname metadata.local resolves to private IP 169.254.169.254");
307
+ });
308
+ it("blocks hostname with any blocked IP in multi-A record", async () => {
309
+ await expect(validateUrl("http://multi-ip.local")).rejects.toThrow("Hostname multi-ip.local resolves to private IP 10.0.0.1");
310
+ });
311
+ it("throws on DNS resolution failure", async () => {
312
+ await expect(validateUrl("http://notfound.local")).rejects.toThrow("Unable to resolve hostname: notfound.local");
313
+ });
314
+ });
315
+ describe("invalid URL handling", () => {
316
+ it("rejects malformed URL", async () => {
317
+ await expect(validateUrl("not-a-url")).rejects.toThrow("Invalid URL format");
318
+ });
319
+ it("rejects URL with invalid characters", async () => {
320
+ await expect(validateUrl("http://example .com")).rejects.toThrow("Invalid URL format");
321
+ });
322
+ });
323
+ describe("URL preservation", () => {
324
+ it("preserves path and query parameters", async () => {
325
+ const result = await validateUrl("http://example.com/path?query=value");
326
+ expect(result.url.pathname).toBe("/path");
327
+ expect(result.url.search).toBe("?query=value");
328
+ });
329
+ it("preserves port", async () => {
330
+ const result = await validateUrl("http://example.com:8080");
331
+ expect(result.url.port).toBe("8080");
332
+ });
333
+ });
334
+ });
335
+ describe("safeFetch", () => {
336
+ beforeEach(() => {
337
+ // Mock DNS for safeFetch tests
338
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
339
+ vi.spyOn(dns, "lookup").mockImplementation((async (hostname) => {
340
+ if (hostname === "private.local") {
341
+ return [{ address: "10.0.0.1", family: 4 }];
342
+ }
343
+ // Don't provide mock for example.com - tests will fail without proper mocking
344
+ throw new Error(`Mock not configured for hostname: ${hostname}`);
345
+ }));
346
+ });
347
+ afterEach(() => {
348
+ vi.restoreAllMocks();
349
+ vi.unstubAllGlobals();
350
+ });
351
+ describe("URL validation", () => {
352
+ it("rejects blocked URLs", async () => {
353
+ await expect(safeFetch("http://private.local")).rejects.toThrow("Hostname private.local resolves to private IP 10.0.0.1");
354
+ });
355
+ it("allows public IP addresses without DNS lookup", async () => {
356
+ // Mock fetch to verify it gets called
357
+ const mockFetch = vi.fn(async () => {
358
+ return new Response("success", { status: 200 });
359
+ });
360
+ vi.stubGlobal("fetch", mockFetch);
361
+ const response = await safeFetch("http://8.8.8.8/test");
362
+ expect(response.status).toBe(200);
363
+ expect(mockFetch).toHaveBeenCalledWith("http://8.8.8.8/test", expect.objectContaining({ redirect: "manual" }));
364
+ });
365
+ });
366
+ describe("redirect handling", () => {
367
+ it("throws on too many redirects", async () => {
368
+ // Call with remainingRedirects=0 to trigger immediate error
369
+ await expect(safeFetch("http://8.8.8.8", undefined, 0)).rejects.toThrow("Too many redirects");
370
+ });
371
+ it("validates each redirect target independently", async () => {
372
+ // Start with a valid public IP
373
+ const mockFetch = vi.fn(async (url) => {
374
+ if (url.includes("8.8.8.8")) {
375
+ // Redirect to a private IP (should be blocked)
376
+ return new Response(null, {
377
+ status: 302,
378
+ headers: { location: "http://10.0.0.1/evil" },
379
+ });
380
+ }
381
+ return new Response("should not reach here", { status: 200 });
382
+ });
383
+ vi.stubGlobal("fetch", mockFetch);
384
+ // Initial URL is valid, but redirect target is blocked
385
+ await expect(safeFetch("http://8.8.8.8/start", undefined, 10)).rejects.toThrow("Access to private IP 10.0.0.1 is blocked");
386
+ });
387
+ });
388
+ });
389
+ //# sourceMappingURL=ssrf-protection.test.js.map