crawlforge-mcp-server 5.5.4 → 5.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "5.5.4",
3
+ "version": "5.5.6",
4
4
  "mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
5
5
  "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 29 web scraping, crawling, deep-research, and autonomous-extraction tools. Returns clean Markdown and structured JSON for Claude, Cursor, and any MCP client. Defaults to local Ollama for LLM extraction (no API key needed); OpenAI/Anthropic available as opt-in. Includes a unified multi-format scrape tool, an autonomous agent, pre-built site templates, and Camoufox stealth browsing.",
6
6
  "main": "server.js",
@@ -114,7 +114,7 @@
114
114
  "cheerio": "^1.1.2",
115
115
  "commander": "^14.0.3",
116
116
  "compromise": "^14.14.4",
117
- "crawlforge-extractors": "^1.5.1",
117
+ "crawlforge-extractors": "^1.5.3",
118
118
  "diff": "^9.0.0",
119
119
  "dotenv": "^17.2.1",
120
120
  "franc": "^6.2.0",
package/server.js CHANGED
@@ -105,7 +105,7 @@ const taskStore = createTaskStore({ logger });
105
105
  // Create the server
106
106
  const server = new McpServer({
107
107
  name: "crawlforge",
108
- version: "5.5.4",
108
+ version: "5.5.6",
109
109
  description: "Production-ready MCP server with 29 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, deep research, structured extraction, embedded JavaScript state extraction, real Google SERP rank tracking, Reddit search via community archives, change tracking, local-LLM extraction via Ollama, unified multi-format scrape, and autonomous agent tool.",
110
110
  homepage: "https://www.crawlforge.dev",
111
111
  icon: "https://www.crawlforge.dev/icon.png",
package/setup.js CHANGED
@@ -68,8 +68,11 @@ function addToMcpConfig(configPath, clientName, apiKey) {
68
68
  }
69
69
  };
70
70
 
71
- // Write updated config
72
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
71
+ // Write updated config. This file now embeds the API key, so keep it
72
+ // owner-only (0o600). The `mode` option only applies when writeFileSync
73
+ // creates the file, so chmod enforces it for a pre-existing config too.
74
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
75
+ fs.chmodSync(configPath, 0o600);
73
76
 
74
77
  return {
75
78
  success: true,
@@ -107,7 +110,7 @@ function configureMcpClients(apiKey) {
107
110
  if (!fs.existsSync(cursorConfigPath)) {
108
111
  // Create mcp.json if it doesn't exist
109
112
  try {
110
- fs.writeFileSync(cursorConfigPath, JSON.stringify({ mcpServers: {} }, null, 2));
113
+ fs.writeFileSync(cursorConfigPath, JSON.stringify({ mcpServers: {} }, null, 2), { mode: 0o600 });
111
114
  } catch (e) {
112
115
  // Ignore creation errors
113
116
  }
@@ -8,7 +8,7 @@ import { ResultRanker } from '../tools/search/ranking/ResultRanker.js';
8
8
  import { CacheManager } from './cache/CacheManager.js';
9
9
  import { Logger } from '../utils/Logger.js';
10
10
  import { LLMManager } from './llm/LLMManager.js';
11
- import { safeFetch } from '../utils/ssrfGuard.js';
11
+ import { safeFetch, safeGoto } from '../utils/ssrfGuard.js';
12
12
  import { preflightFetch, browserPreflight } from '../utils/robotsGate.js';
13
13
  import { noteRetryAfter } from '../utils/hostRateLimiter.js';
14
14
  import {
@@ -989,7 +989,9 @@ export class ResearchOrchestrator extends EventEmitter {
989
989
  page = await this._stealthManager.createStealthPage(contextId);
990
990
  }
991
991
  try {
992
- const resp = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: this.stealthTimeoutMs });
992
+ // browserPreflight (robots + platform blocklist) ran in _stealthFetchHtml;
993
+ // safeGoto adds the SSRF/DNS-rebinding guard the browser path also needs.
994
+ const resp = await safeGoto(page, url, { waitUntil: 'domcontentloaded', timeout: this.stealthTimeoutMs });
993
995
  // Do NOT bail on the initial HTTP status: anti-bot challenges (Cloudflare
994
996
  // Turnstile) return 403 on the first response and only resolve to the
995
997
  // real page after their JS runs. Let it settle, then judge by the
@@ -13,6 +13,7 @@ import { z } from 'zod';
13
13
  import crypto from 'crypto';
14
14
  import HumanBehaviorSimulator from '../utils/HumanBehaviorSimulator.js';
15
15
  import { BrowserContextPool } from './BrowserContextPool.js';
16
+ import { safeGoto } from '../utils/ssrfGuard.js';
16
17
 
17
18
  const StealthConfigSchema = z.object({
18
19
  level: z.enum(['basic', 'medium', 'advanced']).default('medium'),
@@ -1776,7 +1777,9 @@ export class StealthBrowserManager {
1776
1777
  const { contextId } = await this.createStealthContext({ ...stealthConfig, engine });
1777
1778
  try {
1778
1779
  const page = await this.createStealthPage(contextId);
1779
- await page.goto(url, { waitUntil: 'domcontentloaded' });
1780
+ // SSRF guard at the navigation boundary: the stealth engine resolves DNS
1781
+ // itself, so a URL/host-only check would miss rebinding to private/metadata IPs.
1782
+ await safeGoto(page, url, { waitUntil: 'domcontentloaded' });
1780
1783
  if (wait_for > 0) await page.waitForTimeout(wait_for);
1781
1784
 
1782
1785
  const [title, html, text] = await Promise.all([
@@ -10,8 +10,21 @@
10
10
  import crypto from 'crypto';
11
11
  import dotenv from 'dotenv';
12
12
 
13
- // Load .env file early to check for creator secret
14
- dotenv.config({ path: '.env', quiet: true });
13
+ // Load the cwd .env early to check for the creator secret, but into an isolated
14
+ // object rather than straight into process.env. A .env sitting in whatever
15
+ // directory the server is launched from is untrusted; seeding process.env from
16
+ // it directly would let it silently flip the SSRF controls (this module is
17
+ // imported before config.js runs its package-relative .env load). We copy the
18
+ // benign keys through — preserving the historical "cwd .env is read" behavior,
19
+ // including the creator secret — but never the SSRF/allowlist controls, which
20
+ // must come from the real environment.
21
+ const CWD_ENV_DENYLIST = new Set(['SSRF_PROTECTION_ENABLED', 'SSRF_STRICT', 'ALLOWED_DOMAINS']);
22
+ const cwdEnv = {};
23
+ dotenv.config({ path: '.env', quiet: true, processEnv: cwdEnv });
24
+ for (const [key, value] of Object.entries(cwdEnv)) {
25
+ if (CWD_ENV_DENYLIST.has(key)) continue;
26
+ if (process.env[key] === undefined) process.env[key] = value; // don't override the real env
27
+ }
15
28
 
16
29
  // SECURITY: Clear any externally-set creator mode env var to prevent bypass
17
30
  delete process.env.CRAWLFORGE_CREATOR_MODE;
@@ -9,6 +9,7 @@ import { z } from 'zod';
9
9
  import StealthBrowserManager from '../StealthBrowserManager.js';
10
10
  import HumanBehaviorSimulator from '../../utils/HumanBehaviorSimulator.js';
11
11
  import LocalizationManager from '../LocalizationManager.js';
12
+ import { safeGoto } from '../../utils/ssrfGuard.js';
12
13
 
13
14
  const BrowserProcessorSchema = z.object({
14
15
  url: z.string().url(),
@@ -613,8 +614,10 @@ export class BrowserProcessor {
613
614
  async navigateAndWait(page, url, options) {
614
615
  const startTime = Date.now();
615
616
 
616
- // Navigate to URL
617
- await page.goto(url, {
617
+ // Navigate to URL. safeGoto adds the SSRF/DNS-rebinding guard on top of the
618
+ // robots/blocklist preflight the calling tool already ran, and re-checks the
619
+ // landed URL so a redirect can't carry us into a private/metadata range.
620
+ await safeGoto(page, url, {
618
621
  waitUntil: 'domcontentloaded',
619
622
  timeout: 30000
620
623
  });
@@ -151,7 +151,14 @@ function sendRpcError(res, status, code, message) {
151
151
  */
152
152
  export async function connectStreamableHttp(server, authManager, logger, options = {}) {
153
153
  const port = options.port ?? 3000;
154
- const host = options.host ?? '0.0.0.0';
154
+ // Bind loopback by default so a local `--http` server is not exposed to the
155
+ // LAN. Managed hosts need 0.0.0.0 to be reachable by their router: Render sets
156
+ // $RENDER; $MCP_HTTP_HOST is an explicit override for any other platform.
157
+ const host = options.host ?? process.env.MCP_HTTP_HOST ?? (process.env.RENDER ? '0.0.0.0' : '127.0.0.1');
158
+ const hostIsLoopback = host === '127.0.0.1' || host === '::1' || host === 'localhost';
159
+ if (authManager.isCreatorMode() && !hostIsLoopback) {
160
+ console.error(`WARNING: creator mode is enabled but the server is bound to ${host} (non-loopback) — per-request auth will NOT be bypassed. Bind to 127.0.0.1 to use creator mode.`);
161
+ }
155
162
  const legacy = options.legacy === true;
156
163
  const oauthProvider = options.oauth ?? null;
157
164
  const metrics = options.metrics ?? null;
@@ -242,12 +249,14 @@ export async function connectStreamableHttp(server, authManager, logger, options
242
249
 
243
250
  // MCP endpoint
244
251
  if (req.url === '/mcp' || req.url === '/' || req.url?.startsWith('/mcp?')) {
245
- // Per-request auth (bypassed in creator mode). `internal` marks a
246
- // request from the website's REST proxy (INTERNAL_PROXY_SECRET): it is
247
- // billing-exempt in withAuth because the website already charged the
248
- // end user. Request-scoped only — never persisted on the session.
252
+ // Per-request auth. Creator mode bypasses it, but only on a loopback bind
253
+ // — never expose an unauthenticated MCP endpoint on a public interface.
254
+ // `internal` marks a request from the website's REST proxy
255
+ // (INTERNAL_PROXY_SECRET): it is billing-exempt in withAuth because the
256
+ // website already charged the end user. Request-scoped only — never
257
+ // persisted on the session.
249
258
  let internal = false;
250
- if (!authManager.isCreatorMode()) {
259
+ if (!(authManager.isCreatorMode() && hostIsLoopback)) {
251
260
  const authResult = await authenticateRequest(req, authManager, oauthProvider);
252
261
  if (!authResult.ok) {
253
262
  logger.warn('Streamable HTTP request rejected', {
@@ -323,6 +323,30 @@ export async function safeFetch(url, options = {}) {
323
323
  }
324
324
  }
325
325
 
326
+ /**
327
+ * SSRF-safe wrapper around Playwright's `page.goto`. The navigate analog of
328
+ * safeFetch: a browser drives its own DNS resolution, so URL/host checks alone
329
+ * miss DNS-rebinding and private-IP targets — resolveDns:true closes that.
330
+ * Checks the URL before navigating, then re-checks the landed URL, because a
331
+ * redirect during navigation could carry us into a blocked range even when the
332
+ * original URL was safe. Returns the Response so callers can read the status.
333
+ * Options pass through unchanged; behaviour-preserving for allowed URLs.
334
+ *
335
+ * @param {import('playwright').Page} page
336
+ * @param {string} url
337
+ * @param {Parameters<import('playwright').Page['goto']>[1]} [options]
338
+ * @returns {Promise<import('playwright').Response|null>}
339
+ */
340
+ export async function safeGoto(page, url, options = {}) {
341
+ await assertUrlAllowed(url, { resolveDns: true });
342
+ const response = await page.goto(url, options);
343
+ const landedUrl = page.url();
344
+ if (/^https?:\/\//i.test(landedUrl)) {
345
+ await assertUrlAllowed(landedUrl, { resolveDns: true });
346
+ }
347
+ return response;
348
+ }
349
+
326
350
  // Exposed for unit tests.
327
351
  export const __ssrfInternals = {
328
352
  ssrfLookup,
@@ -1,120 +0,0 @@
1
- /**
2
- * Critical Security Patches for Wave 3 Features
3
- *
4
- * This file contains emergency patches for critical vulnerabilities
5
- * identified in the security audit. Apply these patches immediately
6
- * before any production deployment.
7
- */
8
-
9
- import Wave3Security from './wave3-security.js';
10
- const { SSRFProtection, PathSecurity, InputSecurity, CryptoSecurity, BrowserSecurity } = Wave3Security;
11
-
12
- /**
13
- * CRITICAL PATCH 1: SSRF Protection for Research Tool
14
- * Fixes: CVE-001 - Server-Side Request Forgery
15
- */
16
- export function patchResearchOrchestrator(ResearchOrchestrator) {
17
- const originalConductResearch = ResearchOrchestrator.prototype.conductResearch;
18
-
19
- ResearchOrchestrator.prototype.conductResearch = async function(topic, options = {}) {
20
- // Input validation
21
- if (typeof topic !== 'string' || topic.length < 3 || topic.length > 500) {
22
- throw new Error('Invalid topic: must be string between 3-500 characters');
23
- }
24
-
25
- // Sanitize topic to prevent script injection
26
- const sanitizedTopic = InputSecurity.sanitizeString(topic);
27
-
28
- // Validate and limit research parameters
29
- const secureOptions = {
30
- ...options,
31
- maxUrls: Math.min(options.maxUrls || 10, 50),
32
- timeLimit: Math.min(options.timeLimit || 60000, 120000),
33
- maxDepth: Math.min(Math.max(options.maxDepth || 3, 1), 5),
34
- concurrency: Math.min(options.concurrency || 3, 5)
35
- };
36
-
37
- return originalConductResearch.call(this, sanitizedTopic, secureOptions);
38
- };
39
- }
40
-
41
- /**
42
- * CRITICAL PATCH 2: Browser Security Hardening
43
- * Fixes: CVE-002, CVE-007 - Script Injection and Browser Security
44
- */
45
- export function patchStealthBrowserManager(StealthBrowserManager) {
46
- // Override dangerous browser args
47
- const originalLaunchStealthBrowser = StealthBrowserManager.prototype.launchStealthBrowser;
48
-
49
- StealthBrowserManager.prototype.launchStealthBrowser = async function(config = {}) {
50
- // Force secure browser configuration
51
- const secureConfig = {
52
- ...config,
53
- level: config.level === 'advanced' ? 'medium' : config.level
54
- };
55
-
56
- // Override with secure browser args
57
- this.secureArgs = BrowserSecurity.getSecureBrowserArgs();
58
-
59
- return originalLaunchStealthBrowser.call(this, secureConfig);
60
- };
61
- }
62
-
63
- /**
64
- * CRITICAL PATCH 3: Path Traversal Protection
65
- * Fixes: CVE-003 - Arbitrary File System Access
66
- */
67
- export function patchSnapshotManager(SnapshotManager) {
68
- // Secure file operations
69
- const originalWriteSnapshotFile = SnapshotManager.prototype.writeSnapshotFile;
70
-
71
- SnapshotManager.prototype.writeSnapshotFile = async function(snapshotId, content) {
72
- // Validate snapshot ID
73
- const safeSnapshotId = PathSecurity.validateSnapshotId(snapshotId);
74
-
75
- // Validate content size
76
- InputSecurity.validateContentSize(content);
77
-
78
- // Ensure safe file path
79
- const safePath = PathSecurity.sanitizePath(`${safeSnapshotId}.snap`, this.options.storageDir);
80
-
81
- return originalWriteSnapshotFile.call(this, safeSnapshotId, content);
82
- };
83
-
84
- // Override generateSnapshotId to use cryptographically secure generation
85
- SnapshotManager.prototype.generateSnapshotId = function(url, timestamp) {
86
- return CryptoSecurity.generateSecureId(16);
87
- };
88
- }
89
-
90
- /**
91
- * Apply all critical patches
92
- */
93
- export function applyAllSecurityPatches(components) {
94
- console.log('🔒 Applying Wave 3 security patches...');
95
-
96
- if (components.ResearchOrchestrator) {
97
- patchResearchOrchestrator(components.ResearchOrchestrator);
98
- console.log('✅ ResearchOrchestrator patched');
99
- }
100
-
101
- if (components.StealthBrowserManager) {
102
- patchStealthBrowserManager(components.StealthBrowserManager);
103
- console.log('✅ StealthBrowserManager patched');
104
- }
105
-
106
- if (components.SnapshotManager) {
107
- patchSnapshotManager(components.SnapshotManager);
108
- console.log('✅ SnapshotManager patched');
109
- }
110
-
111
- console.log('🛡️ All Wave 3 security patches applied successfully');
112
- console.log('⚠️ Remember to run security tests before deployment');
113
- }
114
-
115
- export default {
116
- patchResearchOrchestrator,
117
- patchStealthBrowserManager,
118
- patchSnapshotManager,
119
- applyAllSecurityPatches
120
- };
@@ -1,355 +0,0 @@
1
- /**
2
- * Wave 3 Security Test Suite
3
- * Validates security patches and tests for vulnerabilities
4
- */
5
-
6
- import Wave3Security from './wave3-security.js';
7
- const { SecurityTesting, SSRFProtection, PathSecurity, InputSecurity } = Wave3Security;
8
-
9
- /**
10
- * Test SSRF Protection
11
- */
12
- export async function testSSRFProtection() {
13
- console.log('🧪 Testing SSRF Protection...');
14
-
15
- const maliciousUrls = [
16
- 'http://localhost:3000/admin',
17
- 'http://127.0.0.1:22',
18
- 'http://169.254.169.254/latest/meta-data/',
19
- 'file:///etc/passwd',
20
- 'ftp://internal.server.com',
21
- 'http://10.0.0.1/secret',
22
- 'https://metadata.google.internal/'
23
- ];
24
-
25
- const results = SecurityTesting.testSecurityFunction(
26
- SSRFProtection.validateUrl.bind(SSRFProtection),
27
- maliciousUrls
28
- );
29
-
30
- const blocked = results.filter(r => r.blocked).length;
31
- const total = results.length;
32
-
33
- console.log(`✅ SSRF Test: ${blocked}/${total} malicious URLs blocked`);
34
-
35
- if (blocked !== total) {
36
- console.error('❌ SSRF Protection insufficient!');
37
- results.filter(r => !r.blocked).forEach(r => {
38
- console.error(` 🚨 Not blocked: ${r.payload}`);
39
- });
40
- }
41
-
42
- return { passed: blocked === total, blocked, total };
43
- }
44
-
45
- /**
46
- * Test Path Traversal Protection
47
- */
48
- export function testPathTraversal() {
49
- console.log('🧪 Testing Path Traversal Protection...');
50
-
51
- const maliciousPaths = [
52
- '../../../etc/passwd',
53
- '..\\..\\..\\windows\\system32\\config\\sam',
54
- '/etc/shadow',
55
- 'C:\\Windows\\System32\\drivers\\etc\\hosts',
56
- '....//....//....//etc/passwd',
57
- '../../../../../../../etc/passwd',
58
- '..\\..\\..\\..\\..\\..\\/etc/passwd'
59
- ];
60
-
61
- const results = SecurityTesting.testSecurityFunction(
62
- PathSecurity.validateSnapshotId.bind(PathSecurity),
63
- maliciousPaths
64
- );
65
-
66
- const blocked = results.filter(r => r.blocked).length;
67
- const total = results.length;
68
-
69
- console.log(`✅ Path Traversal Test: ${blocked}/${total} malicious paths blocked`);
70
-
71
- if (blocked !== total) {
72
- console.error('❌ Path Traversal Protection insufficient!');
73
- results.filter(r => !r.blocked).forEach(r => {
74
- console.error(` 🚨 Not blocked: ${r.payload}`);
75
- });
76
- }
77
-
78
- return { passed: blocked === total, blocked, total };
79
- }
80
-
81
- /**
82
- * Test Input Validation
83
- */
84
- export function testInputValidation() {
85
- console.log('🧪 Testing Input Validation...');
86
-
87
- const maliciousInputs = [
88
- '<script>alert("XSS")</script>',
89
- 'javascript:alert("XSS")',
90
- '<img src="x" onerror="alert(\'XSS\')">',
91
- '<svg onload="alert(\'XSS\')"></svg>',
92
- '${7*7}',
93
- '#{7*7}',
94
- '{{7*7}}',
95
- '<%= 7*7 %>'
96
- ];
97
-
98
- const results = maliciousInputs.map(input => {
99
- try {
100
- const sanitized = InputSecurity.sanitizeHtml(input);
101
- const isBlocked = !sanitized.includes('<script') &&
102
- !sanitized.includes('javascript:') &&
103
- !sanitized.includes('onerror=') &&
104
- !sanitized.includes('onload=');
105
- return { input, blocked: isBlocked, sanitized };
106
- } catch (error) {
107
- return { input, blocked: true, error: error.message };
108
- }
109
- });
110
-
111
- const blocked = results.filter(r => r.blocked).length;
112
- const total = results.length;
113
-
114
- console.log(`✅ Input Validation Test: ${blocked}/${total} malicious inputs neutralized`);
115
-
116
- return { passed: blocked === total, blocked, total };
117
- }
118
-
119
- /**
120
- * Test Resource Limits
121
- */
122
- export function testResourceLimits() {
123
- console.log('🧪 Testing Resource Limits...');
124
-
125
- const tests = [
126
- {
127
- name: 'Large Content',
128
- test: () => {
129
- const largeContent = 'x'.repeat(100 * 1024 * 1024); // 100MB
130
- try {
131
- InputSecurity.validateContentSize(largeContent);
132
- return false; // Should have thrown
133
- } catch (error) {
134
- return error.message.includes('Content too large');
135
- }
136
- }
137
- },
138
- {
139
- name: 'Long String',
140
- test: () => {
141
- const longString = 'x'.repeat(20000);
142
- try {
143
- InputSecurity.sanitizeString(longString, 10000);
144
- return false; // Should have thrown
145
- } catch (error) {
146
- return error.message.includes('String too long');
147
- }
148
- }
149
- },
150
- {
151
- name: 'Large Array',
152
- test: () => {
153
- const largeArray = new Array(200).fill('item');
154
- try {
155
- InputSecurity.sanitizeArray(largeArray, 100);
156
- return false; // Should have thrown
157
- } catch (error) {
158
- return error.message.includes('Array too long');
159
- }
160
- }
161
- }
162
- ];
163
-
164
- let passed = 0;
165
- tests.forEach(test => {
166
- if (test.test()) {
167
- console.log(` ✅ ${test.name}: PASS`);
168
- passed++;
169
- } else {
170
- console.log(` ❌ ${test.name}: FAIL`);
171
- }
172
- });
173
-
174
- console.log(`✅ Resource Limits Test: ${passed}/${tests.length} tests passed`);
175
-
176
- return { passed: passed === tests.length, passedCount: passed, total: tests.length };
177
- }
178
-
179
- /**
180
- * Test Cryptographic Functions
181
- */
182
- export function testCryptographicSecurity() {
183
- console.log('🧪 Testing Cryptographic Security...');
184
-
185
- const tests = [
186
- {
187
- name: 'Secure ID Generation',
188
- test: () => {
189
- const id1 = Wave3Security.CryptoSecurity.generateSecureId();
190
- const id2 = Wave3Security.CryptoSecurity.generateSecureId();
191
-
192
- // Should be different
193
- if (id1 === id2) return false;
194
-
195
- // Should be hex and right length
196
- if (!/^[a-f0-9]{64}$/.test(id1)) return false;
197
- if (!/^[a-f0-9]{64}$/.test(id2)) return false;
198
-
199
- return true;
200
- }
201
- },
202
- {
203
- name: 'Timing Safe Comparison',
204
- test: () => {
205
- const hash1 = 'abcd1234';
206
- const hash2 = 'abcd1234';
207
- const hash3 = 'efgh5678';
208
-
209
- // Same hashes should match
210
- if (!Wave3Security.CryptoSecurity.timingSafeCompare(hash1, hash2)) return false;
211
-
212
- // Different hashes should not match
213
- if (Wave3Security.CryptoSecurity.timingSafeCompare(hash1, hash3)) return false;
214
-
215
- return true;
216
- }
217
- },
218
- {
219
- name: 'Webhook Signature',
220
- test: () => {
221
- const payload = JSON.stringify({ test: 'data' });
222
- const secret = 'test-secret';
223
-
224
- const signature1 = Wave3Security.CryptoSecurity.generateWebhookSignature(payload, secret);
225
- const signature2 = Wave3Security.CryptoSecurity.generateWebhookSignature(payload, secret);
226
-
227
- // Should be consistent
228
- if (signature1 !== signature2) return false;
229
-
230
- // Should validate correctly
231
- if (!Wave3Security.CryptoSecurity.validateWebhookSignature(payload, signature1, secret)) return false;
232
-
233
- // Should reject wrong signature
234
- if (Wave3Security.CryptoSecurity.validateWebhookSignature(payload, 'wrong', secret)) return false;
235
-
236
- return true;
237
- }
238
- }
239
- ];
240
-
241
- let passed = 0;
242
- tests.forEach(test => {
243
- if (test.test()) {
244
- console.log(` ✅ ${test.name}: PASS`);
245
- passed++;
246
- } else {
247
- console.log(` ❌ ${test.name}: FAIL`);
248
- }
249
- });
250
-
251
- console.log(`✅ Cryptographic Security Test: ${passed}/${tests.length} tests passed`);
252
-
253
- return { passed: passed === tests.length, passedCount: passed, total: tests.length };
254
- }
255
-
256
- /**
257
- * Run Full Security Test Suite
258
- */
259
- export async function runFullSecurityTests() {
260
- console.log('🛡️ Running Wave 3 Security Test Suite...\n');
261
-
262
- const results = [];
263
-
264
- // Test SSRF Protection
265
- results.push(await testSSRFProtection());
266
- console.log('');
267
-
268
- // Test Path Traversal Protection
269
- results.push(testPathTraversal());
270
- console.log('');
271
-
272
- // Test Input Validation
273
- results.push(testInputValidation());
274
- console.log('');
275
-
276
- // Test Resource Limits
277
- results.push(testResourceLimits());
278
- console.log('');
279
-
280
- // Test Cryptographic Security
281
- results.push(testCryptographicSecurity());
282
- console.log('');
283
-
284
- // Summary
285
- const totalPassed = results.filter(r => r.passed).length;
286
- const totalTests = results.length;
287
-
288
- console.log('🎯 Security Test Summary:');
289
- console.log(` Passed: ${totalPassed}/${totalTests} test suites`);
290
-
291
- if (totalPassed === totalTests) {
292
- console.log('✅ ALL SECURITY TESTS PASSED! Wave 3 appears secure.');
293
- } else {
294
- console.log('❌ SECURITY TESTS FAILED! Do not deploy to production.');
295
- console.log(' Review failed tests and apply additional patches.');
296
- }
297
-
298
- return {
299
- allPassed: totalPassed === totalTests,
300
- passed: totalPassed,
301
- total: totalTests,
302
- results
303
- };
304
- }
305
-
306
- /**
307
- * Quick Security Health Check
308
- */
309
- export function quickSecurityCheck() {
310
- console.log('🚀 Quick Security Health Check...');
311
-
312
- const checks = [
313
- {
314
- name: 'SSRF Protection Available',
315
- check: () => typeof SSRFProtection.validateUrl === 'function'
316
- },
317
- {
318
- name: 'Path Security Available',
319
- check: () => typeof PathSecurity.validateSnapshotId === 'function'
320
- },
321
- {
322
- name: 'Input Sanitization Available',
323
- check: () => typeof InputSecurity.sanitizeString === 'function'
324
- },
325
- {
326
- name: 'Crypto Security Available',
327
- check: () => typeof Wave3Security.CryptoSecurity.generateSecureId === 'function'
328
- }
329
- ];
330
-
331
- let passed = 0;
332
- checks.forEach(check => {
333
- if (check.check()) {
334
- console.log(` ✅ ${check.name}`);
335
- passed++;
336
- } else {
337
- console.log(` ❌ ${check.name}`);
338
- }
339
- });
340
-
341
- console.log(`\n🎯 Health Check: ${passed}/${checks.length} security functions available`);
342
-
343
- return passed === checks.length;
344
- }
345
-
346
- // Export test functions
347
- export default {
348
- testSSRFProtection,
349
- testPathTraversal,
350
- testInputValidation,
351
- testResourceLimits,
352
- testCryptographicSecurity,
353
- runFullSecurityTests,
354
- quickSecurityCheck
355
- };
@@ -1,653 +0,0 @@
1
- /**
2
- * Wave 3 Security Validation Utilities
3
- * Provides security validation, sanitization, and protection functions
4
- * for Wave 3 features in the CrawlForge MCP Server project.
5
- */
6
-
7
- import { z } from 'zod';
8
- import { createHash, randomBytes, timingSafeEqual } from 'crypto';
9
- import path from 'path';
10
- import { URL } from 'url';
11
- import DOMPurify from 'isomorphic-dompurify';
12
- import { identityHeaders } from '../utils/fetchIdentity.js';
13
-
14
- // Security configuration
15
- const SECURITY_CONFIG = {
16
- // SSRF Protection
17
- allowedDomains: [
18
- 'google.com',
19
- 'bing.com',
20
- 'duckduckgo.com',
21
- 'wikipedia.org',
22
- 'archive.org',
23
- '*.edu',
24
- '*.gov'
25
- ],
26
-
27
- blockedDomains: [
28
- 'localhost',
29
- '127.0.0.1',
30
- '0.0.0.0',
31
- '10.*',
32
- '172.16.*',
33
- '192.168.*',
34
- '169.254.*', // AWS metadata
35
- 'metadata.google.internal'
36
- ],
37
-
38
- // Resource limits
39
- maxContentSize: 50 * 1024 * 1024, // 50MB
40
- maxSnapshotSize: 100 * 1024 * 1024, // 100MB
41
- maxResearchUrls: 50,
42
- maxResearchTime: 120000, // 2 minutes
43
-
44
- // Path validation
45
- allowedDirectories: [
46
- './snapshots',
47
- './cache',
48
- './temp'
49
- ],
50
-
51
- // Content validation
52
- maxStringLength: 10000,
53
- maxArrayLength: 100,
54
-
55
- // Rate limiting
56
- defaultRateLimit: {
57
- windowMs: 60 * 1000, // 1 minute
58
- max: 100 // requests per window
59
- }
60
- };
61
-
62
- /**
63
- * SSRF Protection Functions
64
- */
65
- export class SSRFProtection {
66
- static validateUrl(url) {
67
- try {
68
- const parsedUrl = new URL(url);
69
-
70
- // Check protocol
71
- if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
72
- throw new Error('Invalid protocol. Only HTTP and HTTPS are allowed.');
73
- }
74
-
75
- // Check for blocked domains
76
- const hostname = parsedUrl.hostname.toLowerCase();
77
- for (const blocked of SECURITY_CONFIG.blockedDomains) {
78
- if (blocked.includes('*')) {
79
- const pattern = blocked.replace('*', '.*');
80
- if (new RegExp(pattern).test(hostname)) {
81
- throw new Error(`Blocked domain: ${hostname}`);
82
- }
83
- } else if (hostname === blocked || hostname.endsWith(`.${blocked}`)) {
84
- throw new Error(`Blocked domain: ${hostname}`);
85
- }
86
- }
87
-
88
- // Check for allowed domains (if whitelist is used)
89
- if (SECURITY_CONFIG.allowedDomains.length > 0) {
90
- let isAllowed = false;
91
- for (const allowed of SECURITY_CONFIG.allowedDomains) {
92
- if (allowed.includes('*')) {
93
- const pattern = allowed.replace('*', '.*');
94
- if (new RegExp(pattern).test(hostname)) {
95
- isAllowed = true;
96
- break;
97
- }
98
- } else if (hostname === allowed || hostname.endsWith(`.${allowed}`)) {
99
- isAllowed = true;
100
- break;
101
- }
102
- }
103
- if (!isAllowed) {
104
- throw new Error(`Domain not in whitelist: ${hostname}`);
105
- }
106
- }
107
-
108
- // Check for IP addresses (basic check)
109
- const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/;
110
- if (ipPattern.test(hostname)) {
111
- throw new Error('Direct IP access not allowed');
112
- }
113
-
114
- return parsedUrl;
115
- } catch (error) {
116
- if (error instanceof TypeError) {
117
- throw new Error('Invalid URL format');
118
- }
119
- throw error;
120
- }
121
- }
122
-
123
- static async validateAndFetch(url, options = {}) {
124
- const validatedUrl = this.validateUrl(url);
125
-
126
- const fetchOptions = {
127
- timeout: options.timeout || 30000,
128
- headers: {
129
- ...identityHeaders({ role: 'health-check' }),
130
- ...options.headers
131
- },
132
- ...options
133
- };
134
-
135
- // Remove sensitive headers
136
- delete fetchOptions.headers['Authorization'];
137
- delete fetchOptions.headers['Cookie'];
138
-
139
- return fetch(validatedUrl.toString(), fetchOptions);
140
- }
141
- }
142
-
143
- /**
144
- * Path Traversal Protection
145
- */
146
- export class PathSecurity {
147
- static sanitizePath(userPath, baseDirectory) {
148
- // Normalize the path to resolve any .. or . components
149
- const normalizedPath = path.normalize(userPath);
150
-
151
- // Resolve the absolute path
152
- const absolutePath = path.resolve(baseDirectory, normalizedPath);
153
- const absoluteBase = path.resolve(baseDirectory);
154
-
155
- // Ensure the resolved path is within the base directory
156
- if (!absolutePath.startsWith(absoluteBase + path.sep) && absolutePath !== absoluteBase) {
157
- throw new Error('Path traversal detected');
158
- }
159
-
160
- return absolutePath;
161
- }
162
-
163
- static validateSnapshotId(snapshotId) {
164
- // Only allow alphanumeric characters and hyphens
165
- if (!/^[a-zA-Z0-9_-]{1,64}$/.test(snapshotId)) {
166
- throw new Error('Invalid snapshot ID format');
167
- }
168
-
169
- // Prevent path traversal attempts
170
- if (snapshotId.includes('..') || snapshotId.includes('/') || snapshotId.includes('\\')) {
171
- throw new Error('Invalid characters in snapshot ID');
172
- }
173
-
174
- return snapshotId;
175
- }
176
-
177
- static validateDirectory(directory) {
178
- const normalizedDir = path.normalize(directory);
179
-
180
- // Check against allowed directories
181
- for (const allowed of SECURITY_CONFIG.allowedDirectories) {
182
- const absoluteAllowed = path.resolve(allowed);
183
- const absoluteDir = path.resolve(normalizedDir);
184
-
185
- if (absoluteDir.startsWith(absoluteAllowed)) {
186
- return absoluteDir;
187
- }
188
- }
189
-
190
- throw new Error('Directory not allowed');
191
- }
192
- }
193
-
194
- /**
195
- * Input Validation and Sanitization
196
- */
197
- export class InputSecurity {
198
- static sanitizeHtml(html) {
199
- return DOMPurify.sanitize(html, {
200
- ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
201
- ALLOWED_ATTR: []
202
- });
203
- }
204
-
205
- static sanitizeString(str, maxLength = SECURITY_CONFIG.maxStringLength) {
206
- if (typeof str !== 'string') {
207
- throw new Error('Input must be a string');
208
- }
209
-
210
- if (str.length > maxLength) {
211
- throw new Error(`String too long. Maximum length is ${maxLength}`);
212
- }
213
-
214
- // Remove null bytes and control characters
215
- return str.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
216
- }
217
-
218
- static sanitizeArray(arr, maxLength = SECURITY_CONFIG.maxArrayLength) {
219
- if (!Array.isArray(arr)) {
220
- throw new Error('Input must be an array');
221
- }
222
-
223
- if (arr.length > maxLength) {
224
- throw new Error(`Array too long. Maximum length is ${maxLength}`);
225
- }
226
-
227
- return arr;
228
- }
229
-
230
- static validateContentSize(content) {
231
- const size = Buffer.byteLength(content, 'utf8');
232
- if (size > SECURITY_CONFIG.maxContentSize) {
233
- throw new Error(`Content too large: ${size} bytes (max: ${SECURITY_CONFIG.maxContentSize})`);
234
- }
235
- return size;
236
- }
237
-
238
- static sanitizeJavaScript(jsCode) {
239
- // Remove potentially dangerous patterns
240
- const dangerousPatterns = [
241
- /eval\s*\(/gi,
242
- /Function\s*\(/gi,
243
- /setTimeout\s*\(/gi,
244
- /setInterval\s*\(/gi,
245
- /document\.write/gi,
246
- /innerHTML/gi,
247
- /outerHTML/gi,
248
- /execScript/gi,
249
- /script:/gi,
250
- /javascript:/gi,
251
- /data:/gi,
252
- /vbscript:/gi
253
- ];
254
-
255
- let sanitized = jsCode;
256
- for (const pattern of dangerousPatterns) {
257
- if (pattern.test(sanitized)) {
258
- throw new Error('Dangerous JavaScript pattern detected');
259
- }
260
- }
261
-
262
- return sanitized;
263
- }
264
- }
265
-
266
- /**
267
- * Cryptographic Security
268
- */
269
- export class CryptoSecurity {
270
- static generateSecureId(length = 32) {
271
- return randomBytes(length).toString('hex');
272
- }
273
-
274
- static generateSessionId() {
275
- return this.generateSecureId(16);
276
- }
277
-
278
- static hashContent(content, algorithm = 'sha256') {
279
- return createHash(algorithm).update(content).digest('hex');
280
- }
281
-
282
- static timingSafeCompare(a, b) {
283
- if (typeof a !== 'string' || typeof b !== 'string') {
284
- throw new Error('Both values must be strings');
285
- }
286
-
287
- if (a.length !== b.length) {
288
- return false;
289
- }
290
-
291
- const bufferA = Buffer.from(a);
292
- const bufferB = Buffer.from(b);
293
-
294
- return timingSafeEqual(bufferA, bufferB);
295
- }
296
-
297
- static generateWebhookSignature(payload, secret) {
298
- return createHash('sha256')
299
- .update(payload + secret)
300
- .digest('hex');
301
- }
302
-
303
- static validateWebhookSignature(payload, signature, secret) {
304
- const expectedSignature = this.generateWebhookSignature(payload, secret);
305
- return this.timingSafeCompare(signature, expectedSignature);
306
- }
307
- }
308
-
309
- /**
310
- * Resource Management Security
311
- */
312
- export class ResourceSecurity {
313
- static validateResearchLimits(options) {
314
- const maxUrls = Math.min(options.maxUrls || 10, SECURITY_CONFIG.maxResearchUrls);
315
- const timeLimit = Math.min(options.timeLimit || 60000, SECURITY_CONFIG.maxResearchTime);
316
- const maxDepth = Math.min(Math.max(options.maxDepth || 3, 1), 5);
317
-
318
- return {
319
- maxUrls,
320
- timeLimit,
321
- maxDepth,
322
- concurrency: Math.min(options.concurrency || 3, 5)
323
- };
324
- }
325
-
326
- static validateSnapshotLimits(options) {
327
- return {
328
- maxSnapshots: Math.min(options.maxSnapshots || 100, 1000),
329
- maxAge: Math.max(options.maxAge || 86400000, 3600000), // Min 1 hour
330
- maxStorageSize: Math.min(options.maxStorageSize || 1073741824, 10737418240), // Max 10GB
331
- compressionThreshold: Math.max(options.compressionThreshold || 1024, 1024)
332
- };
333
- }
334
- }
335
-
336
- /**
337
- * Browser Security
338
- */
339
- export class BrowserSecurity {
340
- static getSecureBrowserArgs() {
341
- return [
342
- // Security-focused args (remove dangerous ones from original)
343
- '--disable-blink-features=AutomationControlled',
344
- '--disable-dev-shm-usage',
345
- '--disable-extensions',
346
- '--disable-plugins',
347
- '--disable-images', // Reduce attack surface
348
- '--disable-javascript-harmony-shipping',
349
- '--disable-background-timer-throttling',
350
- '--disable-backgrounding-occluded-windows',
351
- '--disable-renderer-backgrounding',
352
- '--disable-field-trial-config',
353
- '--disable-back-forward-cache',
354
- '--disable-hang-monitor',
355
- '--disable-prompt-on-repost',
356
- '--disable-sync',
357
- '--disable-translate',
358
- '--metrics-recording-only',
359
- '--no-first-run',
360
- '--safebrowsing-disable-auto-update',
361
- '--password-store=basic',
362
- '--use-mock-keychain',
363
-
364
- // Security enhancements
365
- '--disable-file-system',
366
- '--disable-databases',
367
- '--disable-local-storage',
368
- '--disable-session-storage',
369
- '--disable-application-cache',
370
- '--disable-notifications',
371
- '--disable-geolocation',
372
- '--disable-microphone',
373
- '--disable-camera',
374
-
375
- // DO NOT include these dangerous args from original:
376
- // --no-sandbox (removes critical security boundary)
377
- // --disable-web-security (removes Same Origin Policy)
378
- // --disable-features=VizDisplayCompositor (removes security features)
379
- ];
380
- }
381
-
382
- static validateStealthConfig(config) {
383
- // Ensure stealth mode doesn't compromise security
384
- const secureConfig = {
385
- ...config,
386
- hideWebDriver: true, // This is fine
387
- blockWebRTC: true, // This is fine for privacy
388
- spoofTimezone: true, // This is fine for privacy
389
- randomizeHeaders: true, // This is fine for privacy
390
-
391
- // Remove dangerous options
392
- disableSecurity: false,
393
- allowUnsafeInlineScripts: false,
394
- bypassCSP: false
395
- };
396
-
397
- return secureConfig;
398
- }
399
-
400
- static sanitizeInjectedScript(script) {
401
- // Validate that injected scripts don't contain dangerous patterns
402
- const dangerousPatterns = [
403
- /eval\(/gi,
404
- /Function\(/gi,
405
- /setTimeout\(/gi,
406
- /setInterval\(/gi,
407
- /XMLHttpRequest/gi,
408
- /fetch\(/gi,
409
- /import\(/gi,
410
- /require\(/gi
411
- ];
412
-
413
- for (const pattern of dangerousPatterns) {
414
- if (pattern.test(script)) {
415
- throw new Error('Dangerous script pattern detected');
416
- }
417
- }
418
-
419
- return script;
420
- }
421
- }
422
-
423
- /**
424
- * Validation Schemas for Wave 3 Components
425
- */
426
- export const Wave3SecuritySchemas = {
427
- // Research Tool Security Schema
428
- researchRequest: z.object({
429
- topic: z.string().min(3).max(500).refine(val => !/<script/i.test(val), 'No script tags allowed'),
430
- maxDepth: z.number().min(1).max(5),
431
- maxUrls: z.number().min(1).max(50),
432
- timeLimit: z.number().min(30000).max(120000),
433
- concurrency: z.number().min(1).max(5)
434
- }),
435
-
436
- // Stealth Browser Security Schema
437
- stealthConfig: z.object({
438
- level: z.enum(['basic', 'medium']), // Remove 'advanced' - too dangerous
439
- randomizeFingerprint: z.boolean(),
440
- hideWebDriver: z.boolean(),
441
- blockWebRTC: z.boolean(),
442
- spoofTimezone: z.boolean(),
443
- customUserAgent: z.string().max(500).optional(),
444
- customViewport: z.object({
445
- width: z.number().min(800).max(1920),
446
- height: z.number().min(600).max(1080)
447
- }).optional()
448
- }),
449
-
450
- // Localization Security Schema
451
- localizationConfig: z.object({
452
- countryCode: z.string().length(2).regex(/^[A-Z]{2}$/),
453
- language: z.string().max(10).regex(/^[a-z]{2}-[A-Z]{2}$/),
454
- timezone: z.string().max(50).regex(/^[A-Za-z_/]+$/),
455
- currency: z.string().length(3).regex(/^[A-Z]{3}$/)
456
- }),
457
-
458
- // Snapshot Security Schema
459
- snapshotRequest: z.object({
460
- url: z.string().url().refine(url => {
461
- try {
462
- SSRFProtection.validateUrl(url);
463
- return true;
464
- } catch {
465
- return false;
466
- }
467
- }, 'Invalid or blocked URL'),
468
- content: z.string().max(SECURITY_CONFIG.maxContentSize),
469
- snapshotId: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/).optional()
470
- }),
471
-
472
- // Change Tracking Security Schema
473
- changeTrackingRequest: z.object({
474
- url: z.string().url(),
475
- granularity: z.enum(['page', 'section', 'element', 'text']),
476
- customSelectors: z.array(z.string().max(100)).max(10).optional(),
477
- excludeSelectors: z.array(z.string().max(100)).max(20).optional()
478
- })
479
- };
480
-
481
- /**
482
- * Security Middleware Factory
483
- */
484
- export class SecurityMiddleware {
485
- static rateLimiter(options = SECURITY_CONFIG.defaultRateLimit) {
486
- const requests = new Map();
487
-
488
- return (identifier) => {
489
- const now = Date.now();
490
- const windowStart = now - options.windowMs;
491
-
492
- // Clean old requests
493
- for (const [key, timestamps] of requests.entries()) {
494
- requests.set(key, timestamps.filter(time => time > windowStart));
495
- }
496
-
497
- // Check current requests
498
- const userRequests = requests.get(identifier) || [];
499
- if (userRequests.length >= options.max) {
500
- throw new Error('Rate limit exceeded');
501
- }
502
-
503
- // Add current request
504
- userRequests.push(now);
505
- requests.set(identifier, userRequests);
506
- };
507
- }
508
-
509
- static validateRequest(schema) {
510
- return (data) => {
511
- try {
512
- return schema.parse(data);
513
- } catch (error) {
514
- throw new Error(`Validation failed: ${error.message}`);
515
- }
516
- };
517
- }
518
-
519
- static auditLogger() {
520
- return (operation, details) => {
521
- const logEntry = {
522
- timestamp: new Date().toISOString(),
523
- operation,
524
- details: typeof details === 'object' ? JSON.stringify(details) : details,
525
- level: 'SECURITY'
526
- };
527
-
528
- console.log('[SECURITY AUDIT]', JSON.stringify(logEntry));
529
- };
530
- }
531
- }
532
-
533
- /**
534
- * Security Testing Utilities
535
- */
536
- export class SecurityTesting {
537
- static generateMaliciousPayloads() {
538
- return {
539
- ssrf: [
540
- 'http://localhost:3000/admin',
541
- 'http://127.0.0.1:22',
542
- 'http://169.254.169.254/latest/meta-data/',
543
- 'file:///etc/passwd',
544
- 'ftp://internal.server.com'
545
- ],
546
-
547
- pathTraversal: [
548
- '../../../etc/passwd',
549
- '..\\..\\..\\windows\\system32\\config\\sam',
550
- '/etc/shadow',
551
- 'C:\\Windows\\System32\\drivers\\etc\\hosts'
552
- ],
553
-
554
- xss: [
555
- '<script>alert("XSS")</script>',
556
- 'javascript:alert("XSS")',
557
- '<img src="x" onerror="alert(\'XSS\')">',
558
- '<svg onload="alert(\'XSS\')"></svg>'
559
- ],
560
-
561
- injection: [
562
- "'; DROP TABLE users; --",
563
- '${7*7}',
564
- '#{7*7}',
565
- '{{7*7}}',
566
- '<%= 7*7 %>'
567
- ]
568
- };
569
- }
570
-
571
- static testSecurityFunction(securityFunction, maliciousPayloads) {
572
- const results = [];
573
-
574
- for (const payload of maliciousPayloads) {
575
- try {
576
- securityFunction(payload);
577
- results.push({ payload, blocked: false, error: null });
578
- } catch (error) {
579
- results.push({ payload, blocked: true, error: error.message });
580
- }
581
- }
582
-
583
- return results;
584
- }
585
- }
586
-
587
- /**
588
- * Emergency Security Patches
589
- */
590
- export class EmergencyPatches {
591
- // Patch for ResearchOrchestrator SSRF vulnerability
592
- static patchResearchTool(researchTool) {
593
- const originalExecute = researchTool.execute;
594
-
595
- researchTool.execute = async function(params) {
596
- // Validate research limits
597
- const secureParams = ResourceSecurity.validateResearchLimits(params);
598
-
599
- // Validate topic for script injection
600
- secureParams.topic = InputSecurity.sanitizeString(params.topic, 500);
601
-
602
- return originalExecute.call(this, secureParams);
603
- };
604
- }
605
-
606
- // Patch for StealthBrowserManager script injection
607
- static patchBrowserManager(browserManager) {
608
- const originalCreateStealthContext = browserManager.createStealthContext;
609
-
610
- browserManager.createStealthContext = async function(config) {
611
- // Validate and secure the config
612
- const secureConfig = BrowserSecurity.validateStealthConfig(config);
613
-
614
- // Use secure browser args
615
- secureConfig.browserArgs = BrowserSecurity.getSecureBrowserArgs();
616
-
617
- return originalCreateStealthContext.call(this, secureConfig);
618
- };
619
- }
620
-
621
- // Patch for SnapshotManager path traversal
622
- static patchSnapshotManager(snapshotManager) {
623
- const originalStoreSnapshot = snapshotManager.storeSnapshot;
624
-
625
- snapshotManager.storeSnapshot = async function(url, content, metadata, options) {
626
- // Validate URL
627
- SSRFProtection.validateUrl(url);
628
-
629
- // Validate content size
630
- InputSecurity.validateContentSize(content);
631
-
632
- // Generate secure snapshot ID
633
- const snapshotId = CryptoSecurity.generateSecureId();
634
-
635
- return originalStoreSnapshot.call(this, url, content, metadata, { ...options, snapshotId });
636
- };
637
- }
638
- }
639
-
640
- // Export default security instance
641
- export default {
642
- SSRFProtection,
643
- PathSecurity,
644
- InputSecurity,
645
- CryptoSecurity,
646
- ResourceSecurity,
647
- BrowserSecurity,
648
- Wave3SecuritySchemas,
649
- SecurityMiddleware,
650
- SecurityTesting,
651
- EmergencyPatches,
652
- SECURITY_CONFIG
653
- };