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.
- package/build/index.js +81 -0
- 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.
|
|
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",
|