# Directory Structure ``` ├── .gitignore ├── .nvmrc ├── index.ts ├── LICENSE ├── package.json ├── README.md ├── sql.png ├── tsconfig.json ├── xss.png └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- ``` 1 | v20.16.0 2 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | dist 3 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | <h1 align="center">MCP Server Pentest</h1> 2 | 3 | 4 | ## Features 5 | 6 | - Full browser xss, sql vulnerability automatic detection 7 | - Screenshots of the entire page or specific elements 8 | - Comprehensive network interaction (navigation, clicks, form filling) 9 | - Console log monitoring 10 | - JavaScript execution in the browser context 11 | 12 | ## Installation 13 | 14 | ### Installing 15 | 16 | ``` 17 | npx playwright install firefox 18 | yarn install 19 | npm run build 20 | ``` 21 | 22 | ## Configuration 23 | 24 | The installation process will automatically add the following configuration to your Claude config file: 25 | 26 | ```json 27 | { 28 | "mcpServers": { 29 | "playwright": { 30 | "command": "npx", 31 | "args": [ 32 | "-y", 33 | "/Users/...../dist/index.js" 34 | ], 35 | "disabled": false, 36 | "autoApprove": [] 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ## Components 43 | 44 | ### Tools 45 | 46 | 47 | #### `broser_url_reflected_xss` 48 | Test whether the URL has an XSS vulnerability 49 | ```javascript 50 | { 51 | "url": "https://test.com", 52 | "paramName":"text" 53 | } 54 | ``` 55 |  56 | 57 | 58 | #### `browser_url_sql_injection` 59 | 60 | Test whether the URL has SQL injection vulnerabilities 61 | 62 | ```javascript 63 | { 64 | "url": "https://test.com", 65 | "paramName":"text" 66 | } 67 | ``` 68 | 69 |  70 | 71 | 72 | 73 | 74 | #### `browser_navigate` 75 | Navigate to any URL in the browser 76 | ```javascript 77 | { 78 | "url": "https://stealthbrowser.cloud" 79 | } 80 | ``` 81 | 82 | #### `browser_screenshot` 83 | Capture screenshots of the entire page or specific elements 84 | ```javascript 85 | { 86 | "name": "screenshot-name", // required 87 | "selector": "#element-id", // optional 88 | "fullPage": true // optional, default: false 89 | } 90 | ``` 91 | 92 | #### `browser_click` 93 | Click elements on the page using CSS selector 94 | ```javascript 95 | { 96 | "selector": "#button-id" 97 | } 98 | ``` 99 | 100 | #### `browser_click_text` 101 | Click elements on the page by their text content 102 | ```javascript 103 | { 104 | "text": "Click me" 105 | } 106 | ``` 107 | 108 | #### `browser_hover` 109 | Hover over elements on the page using CSS selector 110 | ```javascript 111 | { 112 | "selector": "#menu-item" 113 | } 114 | ``` 115 | 116 | #### `browser_hover_text` 117 | Hover over elements on the page by their text content 118 | ```javascript 119 | { 120 | "text": "Hover me" 121 | } 122 | ``` 123 | 124 | #### `browser_fill` 125 | Fill out input fields 126 | ```javascript 127 | { 128 | "selector": "#input-field", 129 | "value": "Hello World" 130 | } 131 | ``` 132 | 133 | #### `browser_select` 134 | Select an option in a SELECT element using CSS selector 135 | ```javascript 136 | { 137 | "selector": "#dropdown", 138 | "value": "option-value" 139 | } 140 | ``` 141 | 142 | #### `browser_select_text` 143 | Select an option in a SELECT element by its text content 144 | ```javascript 145 | { 146 | "text": "Choose me", 147 | "value": "option-value" 148 | } 149 | ``` 150 | 151 | #### `browser_evaluate` 152 | Execute JavaScript in the browser console 153 | ```javascript 154 | { 155 | "script": "document.title" 156 | } 157 | ``` 158 | 159 | 160 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "outDir": "./dist", 12 | "rootDir": ".", 13 | }, 14 | "include": [ 15 | "./**/*.ts" 16 | ], 17 | "exclude": ["node_modules"] 18 | } 19 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@automatalabs/mcp-server-playwright", 3 | "version": "1.2.1", 4 | "description": "MCP server for browser automation using Playwright", 5 | "license": "MIT", 6 | "author": "Automata Labs (https://automatalabs.io)", 7 | "homepage": "https://automatalabs.io", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/Automata-Labs-team/MCP-Server-Playwright.git" 11 | }, 12 | "bugs": "https://github.com/Automata-Labs-team/MCP-Server-Playwright/issues", 13 | "type": "module", 14 | "bin": { 15 | "mcp-server-playwright": "dist/index.js" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "build": "tsc && shx chmod +x dist/*.js", 22 | "prepare": "npm run build", 23 | "watch": "tsc --watch" 24 | }, 25 | "dependencies": { 26 | "@modelcontextprotocol/sdk": "0.5.0", 27 | "playwright": "^1.48.0", 28 | "yargs": "^17.7.2" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^22.10.2", 32 | "@types/yargs": "^17.0.33", 33 | "shx": "^0.3.4", 34 | "typescript": "^5.6.2" 35 | } 36 | } ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import yargs from "yargs/yargs"; 4 | import { hideBin } from 'yargs/helpers' 5 | import os from "os"; 6 | import path from "path"; 7 | import { promises as fs } from "fs"; 8 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 9 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 10 | import { 11 | CallToolRequestSchema, 12 | ListResourcesRequestSchema, 13 | ListToolsRequestSchema, 14 | ReadResourceRequestSchema, 15 | CallToolResult, 16 | TextContent, 17 | ImageContent, 18 | Tool, 19 | } from "@modelcontextprotocol/sdk/types.js"; 20 | import playwright, { Browser, Page } from "playwright"; 21 | 22 | enum ToolName { 23 | BrowserNavigate = "browser_navigate", 24 | BrowserScreenshot = "browser_screenshot", 25 | BrowserClick = "browser_click", 26 | BrowserClickText = "browser_click_text", 27 | BrowserFill = "browser_fill", 28 | BrowserSelect = "browser_select", 29 | BrowserSelectText = "browser_select_text", 30 | BrowserHover = "browser_hover", 31 | BrowserHoverText = "browser_hover_text", 32 | BrowserEvaluate = "browser_evaluate", 33 | BrowserUrlReflectedXss = "broser_url_reflected_xss", 34 | BrowserUrlSqlInjection = "browser_url_sql_injection" 35 | } 36 | 37 | // Define the tools once to avoid repetition 38 | const TOOLS: Tool[] = [ 39 | { 40 | name: ToolName.BrowserNavigate, 41 | description: "Navigate to a URL", 42 | inputSchema: { 43 | type: "object", 44 | properties: { 45 | url: { type: "string" }, 46 | }, 47 | required: ["url"], 48 | }, 49 | }, 50 | { 51 | name: ToolName.BrowserScreenshot, 52 | description: "Take a screenshot of the current page or a specific element", 53 | inputSchema: { 54 | type: "object", 55 | properties: { 56 | name: { type: "string", description: "Name for the screenshot" }, 57 | selector: { type: "string", description: "CSS selector for element to screenshot" }, 58 | fullPage: { type: "boolean", description: "Take a full page screenshot (default: false)", default: false }, 59 | }, 60 | required: ["name"], 61 | }, 62 | }, 63 | { 64 | name: ToolName.BrowserClick, 65 | description: "Click an element on the page using CSS selector", 66 | inputSchema: { 67 | type: "object", 68 | properties: { 69 | selector: { type: "string", description: "CSS selector for element to click" }, 70 | }, 71 | required: ["selector"], 72 | }, 73 | }, 74 | { 75 | name: ToolName.BrowserUrlReflectedXss, 76 | description: "Test whether the URL has an XSS vulnerability", 77 | inputSchema: { 78 | type: "object", 79 | properties: { 80 | url: { type: "string" }, 81 | paramName: { type: "string", description: "Parameter name for XSS testing" }, 82 | }, 83 | required: ["url"], 84 | }, 85 | }, 86 | { 87 | name: ToolName.BrowserClickText, 88 | description: "Click an element on the page by its text content", 89 | inputSchema: { 90 | type: "object", 91 | properties: { 92 | text: { type: "string", description: "Text content of the element to click" }, 93 | }, 94 | required: ["text"], 95 | }, 96 | }, 97 | { 98 | name: ToolName.BrowserFill, 99 | description: "Fill out an input field", 100 | inputSchema: { 101 | type: "object", 102 | properties: { 103 | selector: { type: "string", description: "CSS selector for input field" }, 104 | value: { type: "string", description: "Value to fill" }, 105 | }, 106 | required: ["selector", "value"], 107 | }, 108 | }, 109 | { 110 | name: ToolName.BrowserSelect, 111 | description: "Select an element on the page with Select tag using CSS selector", 112 | inputSchema: { 113 | type: "object", 114 | properties: { 115 | selector: { type: "string", description: "CSS selector for element to select" }, 116 | value: { type: "string", description: "Value to select" }, 117 | }, 118 | required: ["selector", "value"], 119 | }, 120 | }, 121 | { 122 | name: ToolName.BrowserSelectText, 123 | description: "Select an element on the page with Select tag by its text content", 124 | inputSchema: { 125 | type: "object", 126 | properties: { 127 | text: { type: "string", description: "Text content of the element to select" }, 128 | value: { type: "string", description: "Value to select" }, 129 | }, 130 | required: ["text", "value"], 131 | }, 132 | }, 133 | { 134 | name: ToolName.BrowserHover, 135 | description: "Hover an element on the page using CSS selector", 136 | inputSchema: { 137 | type: "object", 138 | properties: { 139 | selector: { type: "string", description: "CSS selector for element to hover" }, 140 | }, 141 | required: ["selector"], 142 | }, 143 | }, 144 | { 145 | name: ToolName.BrowserHoverText, 146 | description: "Hover an element on the page by its text content", 147 | inputSchema: { 148 | type: "object", 149 | properties: { 150 | text: { type: "string", description: "Text content of the element to hover" }, 151 | }, 152 | required: ["text"], 153 | }, 154 | }, 155 | { 156 | name: ToolName.BrowserEvaluate, 157 | description: "Execute JavaScript in the browser console", 158 | inputSchema: { 159 | type: "object", 160 | properties: { 161 | script: { type: "string", description: "JavaScript code to execute" }, 162 | }, 163 | required: ["script"], 164 | }, 165 | }, 166 | { 167 | name: ToolName.BrowserUrlSqlInjection, 168 | description: "Test whether the URL has SQL injection vulnerabilities", 169 | inputSchema: { 170 | type: "object", 171 | properties: { 172 | url: { type: "string" }, 173 | paramName: { type: "string", description: "Parameter name for SQL injection testing" }, 174 | }, 175 | required: ["url"], 176 | }, 177 | }, 178 | ]; 179 | 180 | // Global state 181 | let browser: Browser | undefined; 182 | let page: Page | undefined; 183 | const consoleLogs: string[] = []; 184 | const screenshots = new Map<string, string>(); 185 | 186 | async function ensureBrowser() { 187 | if (!browser) { 188 | browser = await playwright.firefox.launch({ headless: false }); 189 | } 190 | 191 | if (!page) { 192 | page = await browser.newPage(); 193 | } 194 | 195 | page.on("console", (msg) => { 196 | const logEntry = `[${msg.type()}] ${msg.text()}`; 197 | consoleLogs.push(logEntry); 198 | server.notification({ 199 | method: "notifications/resources/updated", 200 | params: { uri: "console://logs" }, 201 | }); 202 | }); 203 | return page!; 204 | } 205 | 206 | async function handleToolCall(name: ToolName, args: any): Promise<CallToolResult> { 207 | const page = await ensureBrowser(); 208 | 209 | switch (name) { 210 | case ToolName.BrowserNavigate: 211 | await page.goto(args.url); 212 | return { 213 | content: [{ 214 | type: "text", 215 | text: `Navigated to ${args.url}`, 216 | }], 217 | isError: false, 218 | }; 219 | 220 | case ToolName.BrowserScreenshot: { 221 | const fullPage = (args.fullPage === 'true'); 222 | 223 | const screenshot = await (args.selector ? 224 | page.locator(args.selector).screenshot() : 225 | page.screenshot({ fullPage })); 226 | const base64Screenshot = screenshot.toString('base64'); 227 | 228 | if (!base64Screenshot) { 229 | return { 230 | content: [{ 231 | type: "text", 232 | text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed", 233 | }], 234 | isError: true, 235 | }; 236 | } 237 | 238 | screenshots.set(args.name, base64Screenshot); 239 | server.notification({ 240 | method: "notifications/resources/list_changed", 241 | }); 242 | 243 | return { 244 | content: [ 245 | { 246 | type: "text", 247 | text: `Screenshot '${args.name}' taken`, 248 | } as TextContent, 249 | { 250 | type: "image", 251 | data: base64Screenshot, 252 | mimeType: "image/png", 253 | } as ImageContent, 254 | ], 255 | isError: false, 256 | }; 257 | } 258 | 259 | case ToolName.BrowserClick: 260 | try { 261 | await page.locator(args.selector).click(); 262 | return { 263 | content: [{ 264 | type: "text", 265 | text: `Clicked: ${args.selector}`, 266 | }], 267 | isError: false, 268 | }; 269 | } catch (error) { 270 | if((error as Error).message.includes("strict mode violation")) { 271 | console.log("Strict mode violation, retrying on first element..."); 272 | try { 273 | await page.locator(args.selector).first().click(); 274 | return { 275 | content: [{ 276 | type: "text", 277 | text: `Clicked: ${args.selector}`, 278 | }], 279 | isError: false, 280 | }; 281 | } catch (error) { 282 | return { 283 | content: [{ 284 | type: "text", 285 | text: `Failed (twice) to click ${args.selector}: ${(error as Error).message}`, 286 | }], 287 | isError: true, 288 | }; 289 | } 290 | } 291 | 292 | return { 293 | content: [{ 294 | type: "text", 295 | text: `Failed to click ${args.selector}: ${(error as Error).message}`, 296 | }], 297 | isError: true, 298 | }; 299 | } 300 | 301 | case ToolName.BrowserClickText: 302 | try { 303 | await page.getByText(args.text).click(); 304 | return { 305 | content: [{ 306 | type: "text", 307 | text: `Clicked element with text: ${args.text}`, 308 | }], 309 | isError: false, 310 | }; 311 | } catch (error) { 312 | if((error as Error).message.includes("strict mode violation")) { 313 | console.log("Strict mode violation, retrying on first element..."); 314 | try { 315 | await page.getByText(args.text).first().click(); 316 | return { 317 | content: [{ 318 | type: "text", 319 | text: `Clicked element with text: ${args.text}`, 320 | }], 321 | isError: false, 322 | }; 323 | } catch (error) { 324 | return { 325 | content: [{ 326 | type: "text", 327 | text: `Failed (twice) to click element with text ${args.text}: ${(error as Error).message}`, 328 | }], 329 | isError: true, 330 | }; 331 | } 332 | } 333 | return { 334 | content: [{ 335 | type: "text", 336 | text: `Failed to click element with text ${args.text}: ${(error as Error).message}`, 337 | }], 338 | isError: true, 339 | }; 340 | } 341 | 342 | case ToolName.BrowserFill: 343 | try { 344 | await page.locator(args.selector).pressSequentially(args.value, { delay: 100 }); 345 | return { 346 | content: [{ 347 | type: "text", 348 | text: `Filled ${args.selector} with: ${args.value}`, 349 | }], 350 | isError: false, 351 | }; 352 | } catch (error) { 353 | if((error as Error).message.includes("strict mode violation")) { 354 | console.log("Strict mode violation, retrying on first element..."); 355 | try { 356 | await page.locator(args.selector).first().pressSequentially(args.value, { delay: 100 }); 357 | return { 358 | content: [{ 359 | type: "text", 360 | text: `Filled ${args.selector} with: ${args.value}`, 361 | }], 362 | isError: false, 363 | }; 364 | } catch (error) { 365 | return { 366 | content: [{ 367 | type: "text", 368 | text: `Failed (twice) to fill ${args.selector}: ${(error as Error).message}`, 369 | }], 370 | isError: true, 371 | }; 372 | } 373 | } 374 | return { 375 | content: [{ 376 | type: "text", 377 | text: `Failed to fill ${args.selector}: ${(error as Error).message}`, 378 | }], 379 | isError: true, 380 | }; 381 | } 382 | 383 | case ToolName.BrowserSelect: 384 | try { 385 | await page.locator(args.selector).selectOption(args.value); 386 | return { 387 | content: [{ 388 | type: "text", 389 | text: `Selected ${args.selector} with: ${args.value}`, 390 | }], 391 | isError: false, 392 | }; 393 | } catch (error) { 394 | if((error as Error).message.includes("strict mode violation")) { 395 | console.log("Strict mode violation, retrying on first element..."); 396 | try { 397 | await page.locator(args.selector).first().selectOption(args.value); 398 | return { 399 | content: [{ 400 | type: "text", 401 | text: `Selected ${args.selector} with: ${args.value}`, 402 | }], 403 | isError: false, 404 | }; 405 | } catch (error) { 406 | return { 407 | content: [{ 408 | type: "text", 409 | text: `Failed (twice) to select ${args.selector}: ${(error as Error).message}`, 410 | }], 411 | isError: true, 412 | }; 413 | } 414 | } 415 | return { 416 | content: [{ 417 | type: "text", 418 | text: `Failed to select ${args.selector}: ${(error as Error).message}`, 419 | }], 420 | isError: true, 421 | }; 422 | } 423 | 424 | case ToolName.BrowserSelectText: 425 | try { 426 | await page.getByText(args.text).selectOption(args.value); 427 | return { 428 | content: [{ 429 | type: "text", 430 | text: `Selected element with text ${args.text} with value: ${args.value}`, 431 | }], 432 | isError: false, 433 | }; 434 | } catch (error) { 435 | if((error as Error).message.includes("strict mode violation")) { 436 | console.log("Strict mode violation, retrying on first element..."); 437 | try { 438 | await page.getByText(args.text).first().selectOption(args.value); 439 | return { 440 | content: [{ 441 | type: "text", 442 | text: `Selected element with text ${args.text} with value: ${args.value}`, 443 | }], 444 | isError: false, 445 | }; 446 | } catch (error) { 447 | return { 448 | content: [{ 449 | type: "text", 450 | text: `Failed (twice) to select element with text ${args.text}: ${(error as Error).message}`, 451 | }], 452 | isError: true, 453 | }; 454 | } 455 | } 456 | return { 457 | content: [{ 458 | type: "text", 459 | text: `Failed to select element with text ${args.text}: ${(error as Error).message}`, 460 | }], 461 | isError: true, 462 | }; 463 | } 464 | 465 | case ToolName.BrowserHover: 466 | try { 467 | await page.locator(args.selector).hover(); 468 | return { 469 | content: [{ 470 | type: "text", 471 | text: `Hovered ${args.selector}`, 472 | }], 473 | isError: false, 474 | }; 475 | } catch (error) { 476 | if((error as Error).message.includes("strict mode violation")) { 477 | console.log("Strict mode violation, retrying on first element..."); 478 | try { 479 | await page.locator(args.selector).first().hover(); 480 | return { 481 | content: [{ 482 | type: "text", 483 | text: `Hovered ${args.selector}`, 484 | }], 485 | isError: false, 486 | }; 487 | } catch (error) { 488 | return { 489 | content: [{ 490 | type: "text", 491 | text: `Failed to hover ${args.selector}: ${(error as Error).message}`, 492 | }], 493 | isError: true, 494 | }; 495 | } 496 | } 497 | return { 498 | content: [{ 499 | type: "text", 500 | text: `Failed to hover ${args.selector}: ${(error as Error).message}`, 501 | }], 502 | isError: true, 503 | }; 504 | } 505 | 506 | case ToolName.BrowserHoverText: 507 | try { 508 | await page.getByText(args.text).hover(); 509 | return { 510 | content: [{ 511 | type: "text", 512 | text: `Hovered element with text: ${args.text}`, 513 | }], 514 | isError: false, 515 | }; 516 | } catch (error) { 517 | if((error as Error).message.includes("strict mode violation")) { 518 | console.log("Strict mode violation, retrying on first element..."); 519 | try { 520 | await page.getByText(args.text).first().hover(); 521 | return { 522 | content: [{ 523 | type: "text", 524 | text: `Hovered element with text: ${args.text}`, 525 | }], 526 | isError: false, 527 | }; 528 | } catch (error) { 529 | return { 530 | content: [{ 531 | type: "text", 532 | text: `Failed (twice) to hover element with text ${args.text}: ${(error as Error).message}`, 533 | }], 534 | isError: true, 535 | }; 536 | } 537 | } 538 | return { 539 | content: [{ 540 | type: "text", 541 | text: `Failed to hover element with text ${args.text}: ${(error as Error).message}`, 542 | }], 543 | isError: true, 544 | }; 545 | } 546 | 547 | case ToolName.BrowserUrlReflectedXss: { 548 | const baseUrl = args.url; 549 | const paramName = args.paramName || 'name'; 550 | const xssPayloads = [ 551 | "<script>alert(1)</script>", 552 | "\"><script>alert(1)</script>", 553 | "javascript:alert(1)", 554 | "<img src=x onerror=alert(1)>", 555 | "<svg onload=alert(1)>", 556 | "';alert(1);//" 557 | ]; 558 | 559 | let vulnerablePayloads = []; 560 | 561 | for (const payload of xssPayloads) { 562 | const encodedPayload = encodeURIComponent(payload); 563 | const testUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${paramName}=${encodedPayload}`; 564 | 565 | try { 566 | await page.goto(testUrl); 567 | 568 | // 检查页面源代码中是否包含未编码的payload 569 | const content = await page.content(); 570 | const decodedPayload = decodeURIComponent(payload); 571 | 572 | if (content.includes(decodedPayload)) { 573 | vulnerablePayloads.push({ 574 | payload: payload, 575 | url: testUrl 576 | }); 577 | } 578 | 579 | // 检查是否有JavaScript执行 580 | const hasXss = await page.evaluate((testPayload) => { 581 | return document.documentElement.innerHTML.includes(testPayload); 582 | }, payload); 583 | 584 | if (hasXss) { 585 | vulnerablePayloads.push({ 586 | payload: payload, 587 | url: testUrl 588 | }); 589 | } 590 | } catch (error) { 591 | console.error(`Error testing payload ${payload}: ${error}`); 592 | } 593 | } 594 | 595 | if (vulnerablePayloads.length > 0) { 596 | return { 597 | content: [{ 598 | type: "text", 599 | text: `发现反射型XSS漏洞!\n\n可利用的Payload:\n${vulnerablePayloads.map(v => 600 | `Payload: ${v.payload}\nURL: ${v.url}\n` 601 | ).join('\n')}` 602 | }], 603 | isError: false 604 | }; 605 | } else { 606 | return { 607 | content: [{ 608 | type: "text", 609 | text: "未发现明显的反射型XSS漏洞。" 610 | }], 611 | isError: false 612 | }; 613 | } 614 | } 615 | 616 | case ToolName.BrowserUrlSqlInjection: { 617 | const baseUrl = args.url; 618 | const paramName = args.paramName || 'id'; 619 | const sqlPayloads = [ 620 | "1' OR '1'='1", 621 | "1' OR '1'='1' --", 622 | "1' OR '1'='1' #", 623 | "1; DROP TABLE users--", 624 | "1 UNION SELECT null,null,null--", 625 | "1' UNION SELECT null,null,null--", 626 | "admin' --", 627 | "admin' #", 628 | "' OR 1=1--", 629 | "' OR 'x'='x", 630 | "1' AND SLEEP(5)--", 631 | "1' AND BENCHMARK(5000000,MD5(1))--", 632 | "1' WAITFOR DELAY '0:0:5'--" 633 | ]; 634 | 635 | let vulnerablePayloads = []; 636 | let originalResponse = ''; 637 | 638 | try { 639 | // 首先获取原始响应 640 | await page.goto(baseUrl); 641 | originalResponse = await page.content(); 642 | } catch (error) { 643 | console.error(`Error getting original response: ${error}`); 644 | } 645 | 646 | for (const payload of sqlPayloads) { 647 | const encodedPayload = encodeURIComponent(payload); 648 | const testUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${paramName}=${encodedPayload}`; 649 | 650 | try { 651 | const startTime = Date.now(); 652 | await page.goto(testUrl); 653 | const endTime = Date.now(); 654 | const responseTime = endTime - startTime; 655 | 656 | const newResponse = await page.content(); 657 | 658 | // 检查SQL错误关键字 659 | const sqlErrorPatterns = [ 660 | /SQL syntax/i, 661 | /MySQL/i, 662 | /ORA-[0-9][0-9][0-9][0-9]/, 663 | /PostgreSQL/i, 664 | /SQLite/i, 665 | /SQLSTATE/, 666 | /Microsoft SQL/i, 667 | /ODBC Driver/i, 668 | /DB2 SQL/i, 669 | /Warning.*mysql_/i, 670 | /Warning.*pg_/i, 671 | /Warning.*sqlite_/i 672 | ]; 673 | 674 | const hasError = sqlErrorPatterns.some(pattern => pattern.test(newResponse)); 675 | 676 | // 检查响应差异 677 | const isDifferent = originalResponse !== newResponse; 678 | 679 | // 检查时间延迟(针对基于时间的注入) 680 | const hasTimeDelay = responseTime > 5000 && payload.toLowerCase().includes('sleep') || 681 | payload.toLowerCase().includes('benchmark') || 682 | payload.toLowerCase().includes('waitfor'); 683 | 684 | if (hasError || isDifferent || hasTimeDelay) { 685 | vulnerablePayloads.push({ 686 | payload: payload, 687 | url: testUrl, 688 | reason: [ 689 | hasError ? '发现SQL错误信息' : '', 690 | isDifferent ? '响应内容发生变化' : '', 691 | hasTimeDelay ? '发现时间延迟' : '' 692 | ].filter(Boolean).join(', ') 693 | }); 694 | } 695 | } catch (error) { 696 | console.error(`Error testing payload ${payload}: ${error}`); 697 | } 698 | } 699 | 700 | if (vulnerablePayloads.length > 0) { 701 | return { 702 | content: [{ 703 | type: "text", 704 | text: `发现潜在的SQL注入漏洞!\n\n可能的漏洞点:\n${vulnerablePayloads.map(v => 705 | `Payload: ${v.payload}\nURL: ${v.url}\n原因: ${v.reason}\n` 706 | ).join('\n')}` 707 | }], 708 | isError: false 709 | }; 710 | } else { 711 | return { 712 | content: [{ 713 | type: "text", 714 | text: "未发现明显的SQL注入漏洞。" 715 | }], 716 | isError: false 717 | }; 718 | } 719 | } 720 | 721 | case ToolName.BrowserEvaluate: 722 | try { 723 | const result = await page.evaluate((script) => { 724 | const logs: string[] = []; 725 | const originalConsole = { ...console }; 726 | 727 | ['log', 'info', 'warn', 'error'].forEach(method => { 728 | (console as any)[method] = (...args: any[]) => { 729 | logs.push(`[${method}] ${args.join(' ')}`); 730 | (originalConsole as any)[method](...args); 731 | }; 732 | }); 733 | 734 | try { 735 | const result = eval(script); 736 | Object.assign(console, originalConsole); 737 | return { result, logs }; 738 | } catch (error) { 739 | Object.assign(console, originalConsole); 740 | throw error; 741 | } 742 | }, args.script); 743 | 744 | return { 745 | content: [ 746 | { 747 | type: "text", 748 | text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`, 749 | }, 750 | ], 751 | isError: false, 752 | }; 753 | } catch (error) { 754 | return { 755 | content: [{ 756 | type: "text", 757 | text: `Script execution failed: ${(error as Error).message}`, 758 | }], 759 | isError: true, 760 | }; 761 | } 762 | 763 | default: 764 | return { 765 | content: [{ 766 | type: "text", 767 | text: `Unknown tool: ${name}`, 768 | }], 769 | isError: true, 770 | }; 771 | } 772 | } 773 | 774 | const server = new Server( 775 | { 776 | name: "automatalabs/playwright", 777 | version: "0.1.0", 778 | }, 779 | { 780 | capabilities: { 781 | resources: {}, 782 | tools: {}, 783 | }, 784 | }, 785 | ); 786 | 787 | 788 | // Setup request handlers 789 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({ 790 | resources: [ 791 | { 792 | uri: "console://logs", 793 | mimeType: "text/plain", 794 | name: "Browser console logs", 795 | }, 796 | ...Array.from(screenshots.keys()).map(name => ({ 797 | uri: `screenshot://${name}`, 798 | mimeType: "image/png", 799 | name: `Screenshot: ${name}`, 800 | })), 801 | ], 802 | })); 803 | 804 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 805 | const uri = request.params.uri.toString(); 806 | 807 | if (uri === "console://logs") { 808 | return { 809 | contents: [{ 810 | uri, 811 | mimeType: "text/plain", 812 | text: consoleLogs.join("\n"), 813 | }], 814 | }; 815 | } 816 | 817 | if (uri.startsWith("screenshot://")) { 818 | const name = uri.split("://")[1]; 819 | const screenshot = screenshots.get(name); 820 | if (screenshot) { 821 | return { 822 | contents: [{ 823 | uri, 824 | mimeType: "image/png", 825 | blob: screenshot, 826 | }], 827 | }; 828 | } 829 | } 830 | 831 | throw new Error(`Resource not found: ${uri}`); 832 | }); 833 | 834 | 835 | 836 | async function runServer() { 837 | const transport = new StdioServerTransport(); 838 | await server.connect(transport); 839 | 840 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 841 | tools: TOOLS, 842 | })); 843 | 844 | server.setRequestHandler(CallToolRequestSchema, async (request) => 845 | handleToolCall(request.params.name as ToolName, request.params.arguments ?? {}) 846 | ); 847 | } 848 | 849 | async function checkPlatformAndInstall() { 850 | const platform = os.platform(); 851 | if (platform === "win32") { 852 | console.log("Installing MCP Playwright Server for Windows..."); 853 | try { 854 | const configFilePath = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'); 855 | 856 | let config: any; 857 | try { 858 | // Try to read existing config file 859 | const fileContent = await fs.readFile(configFilePath, 'utf-8'); 860 | config = JSON.parse(fileContent); 861 | } catch (error) { 862 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 863 | // Create new config file with mcpServers object 864 | config = { mcpServers: {} }; 865 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8'); 866 | console.log("Created new Claude config file"); 867 | } else { 868 | console.error("Error reading Claude config file:", error); 869 | process.exit(1); 870 | } 871 | } 872 | 873 | // Ensure mcpServers exists 874 | if (!config.mcpServers) { 875 | config.mcpServers = {}; 876 | } 877 | 878 | // Update the playwright configuration 879 | config.mcpServers.playwright = { 880 | command: "npx", 881 | args: ["-y", "@automatalabs/mcp-server-playwright"] 882 | }; 883 | 884 | // Write the updated config back to file 885 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8'); 886 | console.log("✓ Successfully updated Claude configuration"); 887 | 888 | } catch (error) { 889 | console.error("Error during installation:", error); 890 | process.exit(1); 891 | } 892 | } else if (platform === "darwin") { 893 | console.log("Installing MCP Playwright Server for macOS..."); 894 | try { 895 | const configFilePath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); 896 | 897 | let config: any; 898 | try { 899 | // Try to read existing config file 900 | const fileContent = await fs.readFile(configFilePath, 'utf-8'); 901 | config = JSON.parse(fileContent); 902 | } catch (error) { 903 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 904 | // Create new config file with mcpServers object 905 | config = { mcpServers: {} }; 906 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8'); 907 | console.log("Created new Claude config file"); 908 | } else { 909 | console.error("Error reading Claude config file:", error); 910 | process.exit(1); 911 | } 912 | } 913 | 914 | // Ensure mcpServers exists 915 | if (!config.mcpServers) { 916 | config.mcpServers = {}; 917 | } 918 | 919 | // Update the playwright configuration 920 | config.mcpServers.playwright = { 921 | command: "npx", 922 | args: ["-y", "@automatalabs/mcp-server-playwright"] 923 | }; 924 | 925 | // Write the updated config back to file 926 | await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8'); 927 | console.log("✓ Successfully updated Claude configuration"); 928 | 929 | } catch (error) { 930 | console.error("Error during installation:", error); 931 | process.exit(1); 932 | } 933 | } else { 934 | console.error("Unsupported platform:", platform); 935 | process.exit(1); 936 | } 937 | } 938 | 939 | (async () => { 940 | try { 941 | // Parse args but continue with server if no command specified 942 | await yargs(hideBin(process.argv)) 943 | .command('install', 'Install MCP-Server-Playwright dependencies', () => {}, async () => { 944 | await checkPlatformAndInstall(); 945 | // Exit after successful installation 946 | process.exit(0); 947 | }) 948 | .strict() 949 | .help() 950 | .parse(); 951 | 952 | // If we get here, no command was specified, so run the server 953 | await runServer().catch(console.error); 954 | } catch (error) { 955 | console.error('Error:', error); 956 | process.exit(1); 957 | } 958 | })(); 959 | ```