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 +2 -2
- package/server.js +1 -1
- package/setup.js +6 -3
- package/src/core/ResearchOrchestrator.js +4 -2
- package/src/core/StealthBrowserManager.js +4 -1
- package/src/core/creatorMode.js +15 -2
- package/src/core/processing/BrowserProcessor.js +5 -2
- package/src/server/transports/streamableHttp.js +15 -6
- package/src/utils/ssrfGuard.js +24 -0
- package/src/security/security-patches.js +0 -120
- package/src/security/security-tests.js +0 -355
- package/src/security/wave3-security.js +0 -653
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-mcp-server",
|
|
3
|
-
"version": "5.5.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
package/src/core/creatorMode.js
CHANGED
|
@@ -10,8 +10,21 @@
|
|
|
10
10
|
import crypto from 'crypto';
|
|
11
11
|
import dotenv from 'dotenv';
|
|
12
12
|
|
|
13
|
-
// Load .env
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
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', {
|
package/src/utils/ssrfGuard.js
CHANGED
|
@@ -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
|
-
};
|