@contextfort-ai/openclaw-secure 0.1.8 → 0.1.11

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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # AntiVirus for ClawdBot: Prompt Injection Prevention (ashwin@contextfort.ai)
2
+
3
+ Runtime controls for OpenClaw that intercepts child_process calls, enforces approval for external commands via Telegram, and detects prompt injection in command outputs.
4
+
5
+
6
+
7
+ https://github.com/user-attachments/assets/93064c17-9644-403b-8328-1da8014dae89
8
+
9
+
10
+
11
+
12
+ The runtime control live on Telegram.
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ ./start-gateway-with-hook.sh
18
+ ```
19
+
20
+ ## Requirements
21
+
22
+ - `~/.claude/hooks/.env` with `ANTHROPIC_API_KEY=sk-ant-...`
23
+ - Telegram channel configured in OpenClaw
24
+
25
+ ## How It Works
26
+
27
+ 1. Hooks Node.js `child_process` module at startup
28
+ 2. Every spawn/exec call is intercepted before execution
29
+ 3. Read-only commands pass through immediately
30
+ 4. External/write commands require human approval on Telegram
31
+ 5. Command outputs are checked for prompt injection
32
+ 6. If injection detected, next external command is blocked with warning
33
+
34
+ ## Code References
35
+
36
+ | What | Line |
37
+ |------|------|
38
+ | Command categories | `spawn-hook.js:104-108` |
39
+ | Notion read-only detection | `spawn-hook.js:117-122` |
40
+ | GitHub CLI read-only detection | `spawn-hook.js:124-130` |
41
+ | Notion content extraction | `spawn-hook.js:141-156` |
42
+ | Claude injection check | `spawn-hook.js:168-195` |
43
+ | Main intercept logic | `spawn-hook.js:230-280` |
44
+
45
+ ## Command Categories
46
+
47
+ **SKIP_USER_CONFIRMATION** (line 106) - Read-only, no external writes:
48
+ - System info: `whoami`, `pwd`, `hostname`, `uname`, `sw_vers`
49
+ - File reads: `ls`, `cat`, `head`, `tail`, `file`, `wc`
50
+ - Network info: `arp`, `ifconfig`, `networksetup`, `scutil`
51
+ - Notion/GitHub: GET requests, search queries, list/view subcommands
52
+
53
+ **SKIP_RESPONSE_CHECK** (line 105) - Output cannot be attacker-influenced:
54
+ - `whoami`, `pwd`, `echo`, `hostname`, `uname`
55
+
56
+ **INTERNAL_COMMANDS** (line 107) - Always pass through, even with injection warning:
57
+ - All local system commands that don't touch external services
58
+
59
+ ## Debug Mode
60
+
61
+ ```bash
62
+ SPAWN_GATE_DEBUG=1 ./start-gateway-with-hook.sh
63
+ ```
64
+
65
+ Logs to `spawn-gate.log`, audit trail in `spawn-audit.jsonl`.
@@ -96,7 +96,11 @@ if (args[0] === 'dashboard') {
96
96
  else if (process.platform === 'win32') execSync(`start "" "${openUrl}"`);
97
97
  } catch {}
98
98
 
99
- process.on('SIGINT', () => { server.close(); process.exit(0); });
99
+ process.on('SIGINT', () => {
100
+ if (server._tunnel) try { server._tunnel.kill(); } catch {}
101
+ server.close();
102
+ process.exit(0);
103
+ });
100
104
  // Keep alive — don't fall through
101
105
  return;
102
106
  }
@@ -250,6 +254,77 @@ if (args[0] === 'scan' || args[0] === 'solve') {
250
254
  return; // don't fall through — raw mode is async
251
255
  }
252
256
 
257
+ if (args[0] === 'exfil-allow') {
258
+ const ALLOWLIST_FILE = path.join(CONFIG_DIR, 'exfil_allowlist.json');
259
+ const sub = args[1];
260
+
261
+ function readAllowlist() {
262
+ try { return JSON.parse(fs.readFileSync(ALLOWLIST_FILE, 'utf8')); }
263
+ catch { return { enabled: false, domains: [] }; }
264
+ }
265
+
266
+ function writeAllowlist(data) {
267
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
268
+ fs.writeFileSync(ALLOWLIST_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
269
+ }
270
+
271
+ if (sub === 'list') {
272
+ const al = readAllowlist();
273
+ console.log(`\n Exfil destination allowlist: ${al.enabled ? '\x1b[32menabled\x1b[0m (blocking non-allowed)' : '\x1b[33mdisabled\x1b[0m (log-only mode)'}`);
274
+ if (al.domains.length === 0) {
275
+ console.log(' No domains configured.\n');
276
+ } else {
277
+ console.log(' Allowed destinations:');
278
+ for (const d of al.domains) console.log(` - ${d}`);
279
+ console.log();
280
+ }
281
+ process.exit(0);
282
+ }
283
+
284
+ if (sub === 'add') {
285
+ const domain = args[2];
286
+ if (!domain) { console.error('Usage: openclaw-secure exfil-allow add <domain>'); process.exit(1); }
287
+ const al = readAllowlist();
288
+ if (!al.domains.includes(domain)) al.domains.push(domain);
289
+ al.enabled = true;
290
+ writeAllowlist(al);
291
+ console.log(` Added ${domain} to exfil allowlist. Blocking mode enabled.`);
292
+ console.log(' Restart your openclaw session for changes to take effect.');
293
+ process.exit(0);
294
+ }
295
+
296
+ if (sub === 'remove') {
297
+ const domain = args[2];
298
+ if (!domain) { console.error('Usage: openclaw-secure exfil-allow remove <domain>'); process.exit(1); }
299
+ const al = readAllowlist();
300
+ al.domains = al.domains.filter(d => d !== domain);
301
+ writeAllowlist(al);
302
+ console.log(` Removed ${domain} from exfil allowlist.`);
303
+ if (al.domains.length === 0) console.log(' No domains left — consider disabling with: openclaw-secure exfil-allow disable');
304
+ process.exit(0);
305
+ }
306
+
307
+ if (sub === 'enable') {
308
+ const al = readAllowlist();
309
+ al.enabled = true;
310
+ writeAllowlist(al);
311
+ console.log(' Exfil allowlist blocking enabled. Only allowlisted destinations will be permitted.');
312
+ console.log(' Restart your openclaw session for changes to take effect.');
313
+ process.exit(0);
314
+ }
315
+
316
+ if (sub === 'disable') {
317
+ const al = readAllowlist();
318
+ al.enabled = false;
319
+ writeAllowlist(al);
320
+ console.log(' Exfil allowlist blocking disabled. All exfil detections will be log-only.');
321
+ process.exit(0);
322
+ }
323
+
324
+ console.error('Usage: openclaw-secure exfil-allow <list|add|remove|enable|disable> [domain]');
325
+ process.exit(1);
326
+ }
327
+
253
328
  if (args[0] === 'disable') {
254
329
  try {
255
330
  const original = fs.readFileSync(backupLink, 'utf8').trim();