clawmoat 0.5.0 → 0.8.0
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/CONTRIBUTING.md +4 -2
- package/README.md +86 -3
- package/SECURITY.md +58 -10
- package/bin/clawmoat.js +298 -1
- package/clawmoat-0.8.0.tgz +0 -0
- package/docs/blog/386-malicious-skills.html +255 -0
- package/docs/blog/40000-exposed-openclaw-instances.html +194 -0
- package/docs/blog/agent-trust-protocol.html +197 -0
- package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +223 -0
- package/docs/blog/ibm-experts-agent-runtime-protection.html +238 -0
- package/docs/blog/index.html +168 -0
- package/docs/blog/mcp-30-cves-security-crisis.html +279 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +234 -0
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +369 -0
- package/docs/blog/oasis-websocket-hijack.html +205 -0
- package/docs/blog/ollama-openclaw-security.html +154 -0
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +198 -0
- package/docs/blog/openclaw-security-reckoning-2026.html +361 -0
- package/docs/blog/supply-chain-agents.html +166 -0
- package/docs/blog/supply-chain-agents.md +79 -0
- package/docs/business/index.html +530 -0
- package/docs/business/install.html +247 -0
- package/docs/checklist.html +168 -0
- package/docs/finance/index.html +217 -0
- package/docs/hall-of-fame.html +168 -0
- package/docs/index.html +328 -90
- package/docs/install.sh +557 -0
- package/docs/privacy-policy/index.html +122 -0
- package/docs/scan/index.html +214 -0
- package/docs/sitemap.xml +132 -2
- package/docs/support/index.html +124 -0
- package/docs/terms-of-service/index.html +122 -0
- package/examples/basic-usage.js +38 -0
- package/package.json +1 -1
- package/server/index.js +179 -14
- package/server/index.js.patch +1 -0
- package/src/finance/index.js +585 -0
- package/src/finance/mcp-firewall.js +486 -0
- package/src/guardian/cve-verify.js +129 -0
- package/src/guardian/gateway-monitor.js +590 -0
- package/src/guardian/index.js +3 -1
- package/src/guardian/insider-threat.js +498 -0
- package/src/index.js +3 -0
- package/src/middleware/openclaw.js +28 -1
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMoat Gateway Monitor
|
|
3
|
+
*
|
|
4
|
+
* Detects and mitigates the Oasis Security WebSocket hijack attack
|
|
5
|
+
* (CVE-2026-XXXXX) where any website can silently take full control
|
|
6
|
+
* of an OpenClaw agent via localhost WebSocket brute-force.
|
|
7
|
+
*
|
|
8
|
+
* Attack chain:
|
|
9
|
+
* 1. Malicious website opens WebSocket to localhost:18789
|
|
10
|
+
* 2. Brute-forces gateway password (rate limiter exempts localhost)
|
|
11
|
+
* 3. Auto-registers as trusted device (no user prompt for localhost)
|
|
12
|
+
* 4. Full agent takeover: messages, files, shell commands
|
|
13
|
+
*
|
|
14
|
+
* This module monitors for:
|
|
15
|
+
* - Rapid authentication attempts (brute-force detection)
|
|
16
|
+
* - Unexpected device pairings
|
|
17
|
+
* - WebSocket connections from browser origins
|
|
18
|
+
* - Gateway configuration weaknesses
|
|
19
|
+
*
|
|
20
|
+
* @module clawmoat/guardian/gateway-monitor
|
|
21
|
+
* @see https://www.oasis.security/blog/openclaw-vulnerability
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
|
|
29
|
+
// ─── Constants ──────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Default OpenClaw gateway port */
|
|
32
|
+
const DEFAULT_GATEWAY_PORT = 18789;
|
|
33
|
+
|
|
34
|
+
/** Maximum auth attempts before triggering alert */
|
|
35
|
+
const BRUTE_FORCE_THRESHOLD = 10;
|
|
36
|
+
|
|
37
|
+
/** Time window for brute-force detection (ms) */
|
|
38
|
+
const BRUTE_FORCE_WINDOW_MS = 60_000;
|
|
39
|
+
|
|
40
|
+
/** Maximum new device pairings before alert */
|
|
41
|
+
const PAIRING_THRESHOLD = 3;
|
|
42
|
+
|
|
43
|
+
/** Time window for pairing flood detection (ms) */
|
|
44
|
+
const PAIRING_WINDOW_MS = 300_000;
|
|
45
|
+
|
|
46
|
+
/** Known browser WebSocket origins that indicate cross-origin attack */
|
|
47
|
+
const SUSPICIOUS_ORIGINS = [
|
|
48
|
+
/^https?:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** Gateway config paths to check */
|
|
52
|
+
const GATEWAY_CONFIG_PATHS = [
|
|
53
|
+
path.join(os.homedir(), '.openclaw', 'gateway.json'),
|
|
54
|
+
path.join(os.homedir(), '.openclaw', 'config.json5'),
|
|
55
|
+
path.join(os.homedir(), '.config', 'openclaw', 'gateway.json'),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// ─── Gateway Monitor ────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
class GatewayMonitor {
|
|
61
|
+
/**
|
|
62
|
+
* @param {Object} options
|
|
63
|
+
* @param {number} [options.port=18789] - Gateway port to monitor
|
|
64
|
+
* @param {number} [options.bruteForceThreshold=10] - Auth attempts before alert
|
|
65
|
+
* @param {number} [options.bruteForceWindowMs=60000] - Time window for detection
|
|
66
|
+
* @param {number} [options.pairingThreshold=3] - Max pairings before alert
|
|
67
|
+
* @param {Function} [options.onAlert] - Callback for security alerts
|
|
68
|
+
* @param {string} [options.logPath] - Path to write audit log
|
|
69
|
+
*/
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
this.port = options.port || DEFAULT_GATEWAY_PORT;
|
|
72
|
+
this.bruteForceThreshold = options.bruteForceThreshold || BRUTE_FORCE_THRESHOLD;
|
|
73
|
+
this.bruteForceWindowMs = options.bruteForceWindowMs || BRUTE_FORCE_WINDOW_MS;
|
|
74
|
+
this.pairingThreshold = options.pairingThreshold || PAIRING_THRESHOLD;
|
|
75
|
+
this.pairingWindowMs = options.pairingWindowMs || PAIRING_WINDOW_MS;
|
|
76
|
+
this.onAlert = options.onAlert || null;
|
|
77
|
+
this.logPath = options.logPath || null;
|
|
78
|
+
|
|
79
|
+
// State tracking
|
|
80
|
+
this.authAttempts = []; // { timestamp, source, success }
|
|
81
|
+
this.devicePairings = []; // { timestamp, deviceId, source, autoApproved }
|
|
82
|
+
this.wsConnections = []; // { timestamp, origin, source }
|
|
83
|
+
this.alerts = []; // { timestamp, type, severity, message, details }
|
|
84
|
+
this.knownDevices = new Set();
|
|
85
|
+
this.configIssues = [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Authentication Monitoring ──────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Record an authentication attempt and check for brute-force patterns.
|
|
92
|
+
* @param {Object} attempt
|
|
93
|
+
* @param {string} attempt.source - Source IP/identifier
|
|
94
|
+
* @param {boolean} attempt.success - Whether auth succeeded
|
|
95
|
+
* @param {string} [attempt.origin] - WebSocket origin header
|
|
96
|
+
* @param {number} [attempt.timestamp] - Unix ms (defaults to now)
|
|
97
|
+
* @returns {Object} Analysis result
|
|
98
|
+
*/
|
|
99
|
+
recordAuthAttempt(attempt) {
|
|
100
|
+
const record = {
|
|
101
|
+
timestamp: attempt.timestamp || Date.now(),
|
|
102
|
+
source: attempt.source || 'unknown',
|
|
103
|
+
success: !!attempt.success,
|
|
104
|
+
origin: attempt.origin || null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
this.authAttempts.push(record);
|
|
108
|
+
this._pruneOldEntries(this.authAttempts, this.bruteForceWindowMs);
|
|
109
|
+
|
|
110
|
+
const analysis = this._analyzeAuthPatterns(record);
|
|
111
|
+
|
|
112
|
+
if (this.logPath) {
|
|
113
|
+
this._appendLog({ type: 'auth_attempt', ...record, analysis });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return analysis;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Analyze authentication patterns for brute-force indicators.
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
_analyzeAuthPatterns(latestAttempt) {
|
|
124
|
+
const now = latestAttempt.timestamp;
|
|
125
|
+
const windowStart = now - this.bruteForceWindowMs;
|
|
126
|
+
const recentAttempts = this.authAttempts.filter(a => a.timestamp >= windowStart);
|
|
127
|
+
|
|
128
|
+
const result = {
|
|
129
|
+
totalAttempts: recentAttempts.length,
|
|
130
|
+
failedAttempts: recentAttempts.filter(a => !a.success).length,
|
|
131
|
+
uniqueSources: new Set(recentAttempts.map(a => a.source)).size,
|
|
132
|
+
isBruteForce: false,
|
|
133
|
+
isSuspiciousOrigin: false,
|
|
134
|
+
alerts: [],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Check for brute-force
|
|
138
|
+
if (result.failedAttempts >= this.bruteForceThreshold) {
|
|
139
|
+
result.isBruteForce = true;
|
|
140
|
+
const alert = {
|
|
141
|
+
timestamp: now,
|
|
142
|
+
type: 'brute_force_detected',
|
|
143
|
+
severity: 'critical',
|
|
144
|
+
message: `Brute-force attack detected: ${result.failedAttempts} failed auth attempts in ${this.bruteForceWindowMs / 1000}s`,
|
|
145
|
+
details: {
|
|
146
|
+
failedAttempts: result.failedAttempts,
|
|
147
|
+
windowMs: this.bruteForceWindowMs,
|
|
148
|
+
sources: [...new Set(recentAttempts.filter(a => !a.success).map(a => a.source))],
|
|
149
|
+
recommendation: 'Change gateway password immediately. Use 32+ character password. Consider binding to non-localhost IP.',
|
|
150
|
+
reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
this._emitAlert(alert);
|
|
154
|
+
result.alerts.push(alert);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check for suspicious origin
|
|
158
|
+
if (latestAttempt.origin) {
|
|
159
|
+
const isSuspicious = SUSPICIOUS_ORIGINS.some(re => re.test(latestAttempt.origin));
|
|
160
|
+
if (isSuspicious) {
|
|
161
|
+
result.isSuspiciousOrigin = true;
|
|
162
|
+
const alert = {
|
|
163
|
+
timestamp: now,
|
|
164
|
+
type: 'suspicious_websocket_origin',
|
|
165
|
+
severity: 'critical',
|
|
166
|
+
message: `WebSocket connection from suspicious origin: ${latestAttempt.origin}`,
|
|
167
|
+
details: {
|
|
168
|
+
origin: latestAttempt.origin,
|
|
169
|
+
source: latestAttempt.source,
|
|
170
|
+
recommendation: 'This may be a cross-origin WebSocket hijack attempt. Verify no unauthorized browser tabs are connecting to your gateway.',
|
|
171
|
+
reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
this._emitAlert(alert);
|
|
175
|
+
result.alerts.push(alert);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for rapid successful auth (may indicate stolen credentials)
|
|
180
|
+
const successfulAttempts = recentAttempts.filter(a => a.success);
|
|
181
|
+
const uniqueSuccessSources = new Set(successfulAttempts.map(a => a.source));
|
|
182
|
+
if (uniqueSuccessSources.size > 2) {
|
|
183
|
+
const alert = {
|
|
184
|
+
timestamp: now,
|
|
185
|
+
type: 'multiple_auth_sources',
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
message: `Authenticated from ${uniqueSuccessSources.size} different sources in ${this.bruteForceWindowMs / 1000}s`,
|
|
188
|
+
details: {
|
|
189
|
+
sources: [...uniqueSuccessSources],
|
|
190
|
+
recommendation: 'Verify all authentication sources are legitimate. Rotate gateway password if any are unknown.',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
this._emitAlert(alert);
|
|
194
|
+
result.alerts.push(alert);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Device Pairing Monitoring ──────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Record a device pairing event and check for suspicious patterns.
|
|
204
|
+
* @param {Object} pairing
|
|
205
|
+
* @param {string} pairing.deviceId - Device identifier
|
|
206
|
+
* @param {string} [pairing.source] - Source IP
|
|
207
|
+
* @param {boolean} [pairing.autoApproved] - Whether auto-approved
|
|
208
|
+
* @param {string} [pairing.deviceName] - Human-readable device name
|
|
209
|
+
* @param {number} [pairing.timestamp] - Unix ms
|
|
210
|
+
* @returns {Object} Analysis result
|
|
211
|
+
*/
|
|
212
|
+
recordDevicePairing(pairing) {
|
|
213
|
+
const record = {
|
|
214
|
+
timestamp: pairing.timestamp || Date.now(),
|
|
215
|
+
deviceId: pairing.deviceId,
|
|
216
|
+
source: pairing.source || 'unknown',
|
|
217
|
+
autoApproved: !!pairing.autoApproved,
|
|
218
|
+
deviceName: pairing.deviceName || null,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
this.devicePairings.push(record);
|
|
222
|
+
this._pruneOldEntries(this.devicePairings, this.pairingWindowMs);
|
|
223
|
+
|
|
224
|
+
const isNew = !this.knownDevices.has(record.deviceId);
|
|
225
|
+
if (isNew) {
|
|
226
|
+
this.knownDevices.add(record.deviceId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const analysis = this._analyzePairingPatterns(record, isNew);
|
|
230
|
+
|
|
231
|
+
if (this.logPath) {
|
|
232
|
+
this._appendLog({ type: 'device_pairing', ...record, isNew, analysis });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return analysis;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Analyze device pairing patterns.
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
_analyzePairingPatterns(latestPairing, isNew) {
|
|
243
|
+
const now = latestPairing.timestamp;
|
|
244
|
+
const windowStart = now - this.pairingWindowMs;
|
|
245
|
+
const recentPairings = this.devicePairings.filter(p => p.timestamp >= windowStart);
|
|
246
|
+
|
|
247
|
+
const result = {
|
|
248
|
+
isNew,
|
|
249
|
+
totalRecentPairings: recentPairings.length,
|
|
250
|
+
autoApprovedCount: recentPairings.filter(p => p.autoApproved).length,
|
|
251
|
+
isPairingFlood: false,
|
|
252
|
+
isAutoApproveRisk: false,
|
|
253
|
+
alerts: [],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Check for pairing flood
|
|
257
|
+
if (recentPairings.length >= this.pairingThreshold) {
|
|
258
|
+
result.isPairingFlood = true;
|
|
259
|
+
const alert = {
|
|
260
|
+
timestamp: now,
|
|
261
|
+
type: 'pairing_flood',
|
|
262
|
+
severity: 'high',
|
|
263
|
+
message: `${recentPairings.length} device pairings in ${this.pairingWindowMs / 1000}s (threshold: ${this.pairingThreshold})`,
|
|
264
|
+
details: {
|
|
265
|
+
devices: recentPairings.map(p => ({ id: p.deviceId, source: p.source, autoApproved: p.autoApproved })),
|
|
266
|
+
recommendation: 'Review all paired devices. Revoke unknown devices. Disable auto-approve for localhost.',
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
this._emitAlert(alert);
|
|
270
|
+
result.alerts.push(alert);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for auto-approved pairing from localhost (the Oasis attack)
|
|
274
|
+
if (latestPairing.autoApproved && isNew) {
|
|
275
|
+
result.isAutoApproveRisk = true;
|
|
276
|
+
const severity = latestPairing.source === 'localhost' || latestPairing.source === '127.0.0.1' ? 'critical' : 'warning';
|
|
277
|
+
const alert = {
|
|
278
|
+
timestamp: now,
|
|
279
|
+
type: 'auto_approved_pairing',
|
|
280
|
+
severity,
|
|
281
|
+
message: `New device "${latestPairing.deviceName || latestPairing.deviceId}" auto-approved from ${latestPairing.source}`,
|
|
282
|
+
details: {
|
|
283
|
+
deviceId: latestPairing.deviceId,
|
|
284
|
+
deviceName: latestPairing.deviceName,
|
|
285
|
+
source: latestPairing.source,
|
|
286
|
+
recommendation: severity === 'critical'
|
|
287
|
+
? 'CRITICAL: Localhost auto-approve is the exact vector used in the Oasis WebSocket hijack. Disable auto-approve immediately.'
|
|
288
|
+
: 'Verify this device pairing was intentional.',
|
|
289
|
+
reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
this._emitAlert(alert);
|
|
293
|
+
result.alerts.push(alert);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Gateway Configuration Audit ────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Audit the gateway configuration for security weaknesses.
|
|
303
|
+
* Checks for the specific vulnerabilities exploited in the Oasis attack.
|
|
304
|
+
* @returns {Object} Audit results
|
|
305
|
+
*/
|
|
306
|
+
auditGatewayConfig() {
|
|
307
|
+
const issues = [];
|
|
308
|
+
let configFound = false;
|
|
309
|
+
let config = null;
|
|
310
|
+
|
|
311
|
+
// Find and parse gateway config
|
|
312
|
+
for (const configPath of GATEWAY_CONFIG_PATHS) {
|
|
313
|
+
try {
|
|
314
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
315
|
+
// Handle JSON5 (strip comments)
|
|
316
|
+
const cleaned = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
317
|
+
config = JSON.parse(cleaned);
|
|
318
|
+
configFound = true;
|
|
319
|
+
break;
|
|
320
|
+
} catch {
|
|
321
|
+
// Try next path
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!configFound) {
|
|
326
|
+
issues.push({
|
|
327
|
+
severity: 'info',
|
|
328
|
+
issue: 'gateway_config_not_found',
|
|
329
|
+
message: 'Could not find gateway configuration file. Using defaults.',
|
|
330
|
+
recommendation: 'Ensure gateway is configured with strong authentication.',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check 1: Password strength
|
|
335
|
+
if (config) {
|
|
336
|
+
const token = config.auth?.token || config.gatewayToken || config.token;
|
|
337
|
+
if (token) {
|
|
338
|
+
if (token.length < 20) {
|
|
339
|
+
issues.push({
|
|
340
|
+
severity: 'critical',
|
|
341
|
+
issue: 'weak_gateway_password',
|
|
342
|
+
message: `Gateway password is only ${token.length} characters. Oasis attack brute-forces at hundreds of attempts/second.`,
|
|
343
|
+
recommendation: 'Use a 32+ character random password. Run: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"',
|
|
344
|
+
reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
|
|
345
|
+
});
|
|
346
|
+
} else if (token.length < 32) {
|
|
347
|
+
issues.push({
|
|
348
|
+
severity: 'warning',
|
|
349
|
+
issue: 'short_gateway_password',
|
|
350
|
+
message: `Gateway password is ${token.length} characters. Consider using 32+ for brute-force resistance.`,
|
|
351
|
+
recommendation: 'Use a longer random password for maximum security.',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
issues.push({
|
|
356
|
+
severity: 'critical',
|
|
357
|
+
issue: 'no_gateway_password',
|
|
358
|
+
message: 'No gateway authentication configured. Anyone on localhost can connect.',
|
|
359
|
+
recommendation: 'Set a strong gateway token immediately.',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check 2: Binding address
|
|
365
|
+
if (config) {
|
|
366
|
+
const host = config.host || config.gateway?.host || config.bind;
|
|
367
|
+
if (!host || host === '0.0.0.0') {
|
|
368
|
+
issues.push({
|
|
369
|
+
severity: 'critical',
|
|
370
|
+
issue: 'gateway_bound_all_interfaces',
|
|
371
|
+
message: 'Gateway is bound to all interfaces (0.0.0.0). Accessible from any network.',
|
|
372
|
+
recommendation: 'Bind to localhost (127.0.0.1) or a Tailscale/VPN IP only.',
|
|
373
|
+
});
|
|
374
|
+
} else if (host === '127.0.0.1' || host === 'localhost') {
|
|
375
|
+
issues.push({
|
|
376
|
+
severity: 'warning',
|
|
377
|
+
issue: 'gateway_bound_localhost',
|
|
378
|
+
message: 'Gateway bound to localhost. Still vulnerable to Oasis WebSocket attack from browser.',
|
|
379
|
+
recommendation: 'Consider binding to a Tailscale IP to prevent browser-based WebSocket attacks.',
|
|
380
|
+
reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Check 3: Auto-approve settings
|
|
386
|
+
if (config) {
|
|
387
|
+
const autoApprove = config.autoApprove ?? config.gateway?.autoApprove ?? config.pairApproval?.auto;
|
|
388
|
+
if (autoApprove === true || autoApprove === 'localhost') {
|
|
389
|
+
issues.push({
|
|
390
|
+
severity: 'critical',
|
|
391
|
+
issue: 'auto_approve_enabled',
|
|
392
|
+
message: 'Device auto-approve is enabled. This is the exact vector exploited in the Oasis attack.',
|
|
393
|
+
recommendation: 'Disable auto-approve. Require manual confirmation for all device pairings.',
|
|
394
|
+
reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check 4: Rate limiting
|
|
400
|
+
if (config) {
|
|
401
|
+
const rateLimit = config.rateLimit || config.gateway?.rateLimit;
|
|
402
|
+
if (!rateLimit) {
|
|
403
|
+
issues.push({
|
|
404
|
+
severity: 'high',
|
|
405
|
+
issue: 'no_rate_limiting',
|
|
406
|
+
message: 'No rate limiting configured. Gateway can be brute-forced at hundreds of attempts/second.',
|
|
407
|
+
recommendation: 'Enable rate limiting, including for localhost connections.',
|
|
408
|
+
});
|
|
409
|
+
} else if (rateLimit.excludeLocalhost || rateLimit.trustLocalhost) {
|
|
410
|
+
issues.push({
|
|
411
|
+
severity: 'critical',
|
|
412
|
+
issue: 'localhost_rate_limit_exempt',
|
|
413
|
+
message: 'Localhost is exempt from rate limiting. This is the exact configuration exploited in the Oasis attack.',
|
|
414
|
+
recommendation: 'Remove localhost exemption from rate limiting.',
|
|
415
|
+
reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Check 5: Gateway port
|
|
421
|
+
if (config) {
|
|
422
|
+
const port = config.port || config.gateway?.port || DEFAULT_GATEWAY_PORT;
|
|
423
|
+
if (port === DEFAULT_GATEWAY_PORT) {
|
|
424
|
+
issues.push({
|
|
425
|
+
severity: 'low',
|
|
426
|
+
issue: 'default_gateway_port',
|
|
427
|
+
message: `Using default gateway port ${DEFAULT_GATEWAY_PORT}. Easily discoverable.`,
|
|
428
|
+
recommendation: 'Consider using a non-default port to reduce attack surface.',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.configIssues = issues;
|
|
434
|
+
|
|
435
|
+
const result = {
|
|
436
|
+
configFound,
|
|
437
|
+
issues,
|
|
438
|
+
criticalCount: issues.filter(i => i.severity === 'critical').length,
|
|
439
|
+
highCount: issues.filter(i => i.severity === 'high').length,
|
|
440
|
+
warningCount: issues.filter(i => i.severity === 'warning').length,
|
|
441
|
+
score: this._calculateSecurityScore(issues),
|
|
442
|
+
oasisVulnerable: issues.some(i => i.reference?.includes('oasis')),
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
if (result.criticalCount > 0) {
|
|
446
|
+
const alert = {
|
|
447
|
+
timestamp: Date.now(),
|
|
448
|
+
type: 'gateway_audit_critical',
|
|
449
|
+
severity: 'critical',
|
|
450
|
+
message: `Gateway audit found ${result.criticalCount} critical issues`,
|
|
451
|
+
details: {
|
|
452
|
+
issues: issues.filter(i => i.severity === 'critical'),
|
|
453
|
+
score: result.score,
|
|
454
|
+
oasisVulnerable: result.oasisVulnerable,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
this._emitAlert(alert);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (this.logPath) {
|
|
461
|
+
this._appendLog({ type: 'gateway_audit', ...result });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Calculate a security score (0-100) based on config issues.
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
_calculateSecurityScore(issues) {
|
|
472
|
+
let score = 100;
|
|
473
|
+
for (const issue of issues) {
|
|
474
|
+
switch (issue.severity) {
|
|
475
|
+
case 'critical': score -= 25; break;
|
|
476
|
+
case 'high': score -= 15; break;
|
|
477
|
+
case 'warning': score -= 10; break;
|
|
478
|
+
case 'low': score -= 5; break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return Math.max(0, score);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ─── Recommendations ────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Generate a strong gateway token.
|
|
488
|
+
* @returns {string} 64-character hex token
|
|
489
|
+
*/
|
|
490
|
+
static generateStrongToken() {
|
|
491
|
+
return crypto.randomBytes(32).toString('hex');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get hardened gateway configuration recommendations.
|
|
496
|
+
* @returns {Object} Recommended config
|
|
497
|
+
*/
|
|
498
|
+
static getHardenedConfig() {
|
|
499
|
+
return {
|
|
500
|
+
gateway: {
|
|
501
|
+
host: '127.0.0.1', // Or Tailscale IP for remote access
|
|
502
|
+
port: 18700 + Math.floor(Math.random() * 89), // Random non-default port
|
|
503
|
+
token: GatewayMonitor.generateStrongToken(),
|
|
504
|
+
rateLimit: {
|
|
505
|
+
windowMs: 60_000,
|
|
506
|
+
maxAttempts: 5,
|
|
507
|
+
excludeLocalhost: false, // CRITICAL: do NOT exclude localhost
|
|
508
|
+
},
|
|
509
|
+
autoApprove: false, // CRITICAL: require manual device approval
|
|
510
|
+
pairApproval: {
|
|
511
|
+
auto: false,
|
|
512
|
+
requireConfirmation: true,
|
|
513
|
+
notifyOnPairing: true,
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
_comment: 'Generated by ClawMoat gateway-monitor. See: https://www.oasis.security/blog/openclaw-vulnerability',
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ─── Utility Methods ────────────────────────────────────────────
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get all alerts, optionally filtered by severity.
|
|
524
|
+
* @param {string} [minSeverity] - Minimum severity: 'low' | 'warning' | 'high' | 'critical'
|
|
525
|
+
* @returns {Array} Alerts
|
|
526
|
+
*/
|
|
527
|
+
getAlerts(minSeverity) {
|
|
528
|
+
if (!minSeverity) return [...this.alerts];
|
|
529
|
+
const levels = { low: 0, warning: 1, high: 2, critical: 3 };
|
|
530
|
+
const min = levels[minSeverity] || 0;
|
|
531
|
+
return this.alerts.filter(a => (levels[a.severity] || 0) >= min);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get a summary report.
|
|
536
|
+
* @returns {Object} Summary
|
|
537
|
+
*/
|
|
538
|
+
getSummary() {
|
|
539
|
+
return {
|
|
540
|
+
authAttempts: this.authAttempts.length,
|
|
541
|
+
failedAuth: this.authAttempts.filter(a => !a.success).length,
|
|
542
|
+
devicePairings: this.devicePairings.length,
|
|
543
|
+
knownDevices: this.knownDevices.size,
|
|
544
|
+
alerts: this.alerts.length,
|
|
545
|
+
criticalAlerts: this.alerts.filter(a => a.severity === 'critical').length,
|
|
546
|
+
configIssues: this.configIssues.length,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Reset all state (for testing).
|
|
552
|
+
*/
|
|
553
|
+
reset() {
|
|
554
|
+
this.authAttempts = [];
|
|
555
|
+
this.devicePairings = [];
|
|
556
|
+
this.wsConnections = [];
|
|
557
|
+
this.alerts = [];
|
|
558
|
+
this.knownDevices.clear();
|
|
559
|
+
this.configIssues = [];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** @private */
|
|
563
|
+
_emitAlert(alert) {
|
|
564
|
+
this.alerts.push(alert);
|
|
565
|
+
if (this.onAlert) {
|
|
566
|
+
this.onAlert(alert);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** @private */
|
|
571
|
+
_pruneOldEntries(arr, maxAgeMs) {
|
|
572
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
573
|
+
while (arr.length > 0 && arr[0].timestamp < cutoff) {
|
|
574
|
+
arr.shift();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** @private */
|
|
579
|
+
_appendLog(entry) {
|
|
580
|
+
if (!this.logPath) return;
|
|
581
|
+
try {
|
|
582
|
+
const line = JSON.stringify({ ...entry, _ts: new Date().toISOString() }) + '\n';
|
|
583
|
+
fs.appendFileSync(this.logPath, line);
|
|
584
|
+
} catch {
|
|
585
|
+
// Silently fail — don't let logging break monitoring
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
module.exports = { GatewayMonitor, DEFAULT_GATEWAY_PORT, BRUTE_FORCE_THRESHOLD };
|
package/src/guardian/index.js
CHANGED
|
@@ -683,4 +683,6 @@ class CredentialMonitor {
|
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
-
|
|
686
|
+
const { CVEVerifier } = require('./cve-verify');
|
|
687
|
+
|
|
688
|
+
module.exports = { HostGuardian, CredentialMonitor, CVEVerifier, TIERS, FORBIDDEN_ZONES, DANGEROUS_COMMANDS, SAFE_COMMANDS };
|