diff --git a/package.json b/package.json index 567bd18..40ce61b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", "jsdom": "^25.0.1", + "private-ip": "^3.0.2", "turndown": "^7.2.0", "zod": "^3.24.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cda887..6370e39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: jsdom: specifier: ^25.0.1 version: 25.0.1 + private-ip: + specifier: ^3.0.2 + version: 3.0.2 turndown: specifier: ^7.2.0 version: 7.2.0 @@ -210,6 +213,9 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@chainsafe/is-ip@2.1.0': + resolution: {integrity: sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -769,6 +775,14 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-regex@5.0.0: + resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1048,6 +1062,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -1129,6 +1147,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + private-ip@3.0.2: + resolution: {integrity: sha512-2pkOVPGYD/4QyAg95c6E/4bLYXPthT5Xw4ocXYzIIsMBhskOMn6IwkWXmg6ZiA6K58+O6VD/n02r1hDhk7vDPw==} + engines: {node: '>=14.16'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -1646,6 +1668,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@chainsafe/is-ip@2.1.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -2315,6 +2339,10 @@ snapshots: inherits@2.0.4: {} + ip-regex@5.0.0: {} + + ipaddr.js@2.2.0: {} + is-arrayish@0.2.1: {} is-core-module@2.16.0: @@ -2784,6 +2812,8 @@ snapshots: natural-compare@1.4.0: {} + netmask@2.0.2: {} + node-int64@0.4.0: {} node-releases@2.0.19: {} @@ -2853,6 +2883,13 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + private-ip@3.0.2: + dependencies: + '@chainsafe/is-ip': 2.1.0 + ip-regex: 5.0.0 + ipaddr.js: 2.2.0 + netmask: 2.0.2 + prompts@2.4.2: dependencies: kleur: 3.0.3 diff --git a/src/Fetcher.ts b/src/Fetcher.ts index 788681e..83fb95e 100644 --- a/src/Fetcher.ts +++ b/src/Fetcher.ts @@ -1,5 +1,6 @@ import { JSDOM } from "jsdom"; import TurndownService from "turndown"; +import is_ip_private from "private-ip"; import { RequestPayload } from "./types.js"; export class Fetcher { @@ -8,6 +9,11 @@ export class Fetcher { headers, }: RequestPayload): Promise { try { + if (is_ip_private(url)) { + throw new Error( + `Fetcher blocked an attempt to fetch a private IP ${url}. This is to prevent a security vulnerability where a local MCP could fetch privileged local IPs and exfiltrate data.`, + ); + } const response = await fetch(url, { headers: { "User-Agent":