lighthouse-mcp 0.1.12 → 0.1.14

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 (2) hide show
  1. package/build/index.js +81 -0
  2. package/package.json +4 -2
package/build/index.js CHANGED
@@ -7,6 +7,8 @@ import * as chromeLauncher from 'chrome-launcher';
7
7
  import os from 'os';
8
8
  import fs from 'fs';
9
9
  import path from 'path';
10
+ import dns from 'dns/promises';
11
+ import net from 'net';
10
12
  // Workaround for modelcontextprotocol/typescript-sdk#1380
11
13
  // In some Zod runtimes, the method literal is stored under `_def.values[0]`
12
14
  // instead of `_def.value` / `.value`, causing "Schema method literal must be a string"
@@ -38,6 +40,83 @@ Server.prototype.setRequestHandler = function patchedSetRequestHandler(requestSc
38
40
  return originalSetRequestHandler.call(this, requestSchema, handler);
39
41
  }
40
42
  };
43
+ // ---------------------------------------------------------------------------
44
+ // SSRF protection — validate URLs before passing to Lighthouse
45
+ // Allows localhost/loopback (needed for local dev servers) but blocks
46
+ // cloud metadata endpoints, RFC 1918 private ranges, and link-local IPs.
47
+ // ---------------------------------------------------------------------------
48
+ const BLOCKED_IP_RANGES = [
49
+ // RFC 1918 private networks
50
+ { prefix: '10.', mask: null },
51
+ { prefix: '172.', mask: (ip) => { const b = parseInt(ip.split('.')[1], 10); return b >= 16 && b <= 31; } },
52
+ { prefix: '192.168.', mask: null },
53
+ // Link-local (includes AWS metadata 169.254.169.254)
54
+ { prefix: '169.254.', mask: null },
55
+ ];
56
+ const BLOCKED_HOSTNAMES = [
57
+ 'metadata.google.internal',
58
+ 'metadata.goog',
59
+ ];
60
+ function isBlockedIP(ip) {
61
+ for (const range of BLOCKED_IP_RANGES) {
62
+ if (ip.startsWith(range.prefix)) {
63
+ if (range.mask === null || range.mask(ip))
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+ function isLoopback(ip) {
70
+ if (ip === '::1')
71
+ return true;
72
+ if (ip.startsWith('127.'))
73
+ return true;
74
+ return false;
75
+ }
76
+ async function validateUrl(url) {
77
+ let parsed;
78
+ try {
79
+ parsed = new URL(url);
80
+ }
81
+ catch {
82
+ throw new McpError(ErrorCode.InvalidParams, `Invalid URL: ${url}`);
83
+ }
84
+ // Only allow http and https schemes
85
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
86
+ throw new McpError(ErrorCode.InvalidParams, `Unsupported URL scheme "${parsed.protocol}" — only http: and https: are allowed`);
87
+ }
88
+ const hostname = parsed.hostname;
89
+ // Block known cloud metadata hostnames
90
+ if (BLOCKED_HOSTNAMES.includes(hostname.toLowerCase())) {
91
+ throw new McpError(ErrorCode.InvalidParams, `URL hostname "${hostname}" is blocked (cloud metadata endpoint)`);
92
+ }
93
+ // If the hostname is an IP literal, validate it directly
94
+ if (net.isIP(hostname)) {
95
+ if (isLoopback(hostname))
96
+ return; // allow localhost
97
+ if (isBlockedIP(hostname)) {
98
+ throw new McpError(ErrorCode.InvalidParams, `URL resolves to a blocked internal IP address (${hostname})`);
99
+ }
100
+ return;
101
+ }
102
+ // Resolve the hostname and check every returned address
103
+ let addresses;
104
+ try {
105
+ const results = await dns.resolve4(hostname);
106
+ addresses = results;
107
+ }
108
+ catch {
109
+ // If DNS resolution fails, let Lighthouse handle the error naturally
110
+ return;
111
+ }
112
+ for (const ip of addresses) {
113
+ if (isLoopback(ip))
114
+ continue; // allow localhost
115
+ if (isBlockedIP(ip)) {
116
+ throw new McpError(ErrorCode.InvalidParams, `URL hostname "${hostname}" resolves to blocked internal IP address (${ip})`);
117
+ }
118
+ }
119
+ }
41
120
  const isValidAuditArgs = (args) => {
42
121
  return (typeof args === 'object' &&
43
122
  args !== null &&
@@ -144,6 +223,8 @@ class LighthouseServer {
144
223
  if (!isValidAuditArgs(args)) {
145
224
  throw new McpError(ErrorCode.InvalidParams, 'Invalid audit arguments');
146
225
  }
226
+ // SSRF protection: validate the URL before launching Chrome
227
+ await validateUrl(args.url);
147
228
  try {
148
229
  // Ensure temp directory exists and is writable (fixes #19 - Windows EPERM)
149
230
  // On Windows, os.tmpdir() reads TEMP -> TMP -> USERPROFILE, so we verify
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse-mcp",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "MCP server for Google Lighthouse performance metrics",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -41,7 +41,9 @@
41
41
  },
42
42
  "overrides": {
43
43
  "basic-ftp": ">=5.2.0",
44
- "lodash-es": ">=4.17.23"
44
+ "lodash-es": ">=4.17.23",
45
+ "path-to-regexp": ">=8.4.0",
46
+ "brace-expansion": ">=5.0.5"
45
47
  },
46
48
  "devDependencies": {
47
49
  "@types/node": "^20.4.5",