@solongate/proxy 0.19.0 → 0.21.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/hooks/guard.mjs +144 -11
- package/package.json +1 -1
package/hooks/guard.mjs
CHANGED
|
@@ -291,10 +291,13 @@ process.stdin.on('end', async () => {
|
|
|
291
291
|
|
|
292
292
|
// ── Self-protection: block access to hook files and settings ──
|
|
293
293
|
// Hardcoded, no bypass possible — runs before policy/PI config
|
|
294
|
+
// Fully protected: block ALL access (read, write, delete, move)
|
|
294
295
|
const protectedPaths = [
|
|
295
296
|
'.solongate', '.claude', '.cursor', '.gemini', '.antigravity', '.openclaw', '.perplexity',
|
|
296
297
|
'policy.json', '.mcp.json',
|
|
297
298
|
];
|
|
299
|
+
// Write-protected: block write/delete/modify, allow read (cat, grep, head, etc.)
|
|
300
|
+
const writeProtectedPaths = ['.env', '.gitignore'];
|
|
298
301
|
|
|
299
302
|
// Block helper — logs to cloud + exits with code 2
|
|
300
303
|
async function blockSelfProtection(reason) {
|
|
@@ -346,7 +349,7 @@ process.stdin.on('end', async () => {
|
|
|
346
349
|
return stripShellQuotes(decodeAnsiC(s));
|
|
347
350
|
}
|
|
348
351
|
|
|
349
|
-
// 4. Extract inner commands from eval, bash -c, sh -c, pipe to bash/sh
|
|
352
|
+
// 4. Extract inner commands from eval, bash -c, sh -c, pipe to bash/sh, heredoc, process substitution
|
|
350
353
|
function extractInnerCommands(s) {
|
|
351
354
|
const inner = [];
|
|
352
355
|
// eval "cmd" or eval 'cmd' or eval cmd
|
|
@@ -358,6 +361,13 @@ process.stdin.on('end', async () => {
|
|
|
358
361
|
for (const m of s.matchAll(/(?:echo|printf)\s+["']([^"']+)["']\s*\|\s*(?:bash|sh)\b/gi)) inner.push(m[1]);
|
|
359
362
|
// find ... -name "pattern" ... -exec ...
|
|
360
363
|
for (const m of s.matchAll(/-name\s+["']?([^\s"']+)["']?/gi)) inner.push(m[1]);
|
|
364
|
+
// Heredoc: bash << 'EOF'\n...\nEOF or bash <<- EOF\n...\nEOF
|
|
365
|
+
for (const m of s.matchAll(/<<-?\s*['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\s*\1/gi)) inner.push(m[2]);
|
|
366
|
+
// Herestring: bash <<< "cmd"
|
|
367
|
+
for (const m of s.matchAll(/<<<\s*["']([^"']+)["']/gi)) inner.push(m[1]);
|
|
368
|
+
for (const m of s.matchAll(/<<<\s*([^\s;&|]+)/gi)) inner.push(m[1]);
|
|
369
|
+
// Process substitution: <(cmd) or >(cmd)
|
|
370
|
+
for (const m of s.matchAll(/[<>]\(\s*([^)]+)\s*\)/gi)) inner.push(m[1]);
|
|
361
371
|
return inner;
|
|
362
372
|
}
|
|
363
373
|
|
|
@@ -377,25 +387,50 @@ process.stdin.on('end', async () => {
|
|
|
377
387
|
|
|
378
388
|
// 6. Check if a glob/wildcard could match any protected path
|
|
379
389
|
function globMatchesProtected(s) {
|
|
380
|
-
|
|
390
|
+
const hasWild = s.includes('*') || s.includes('?') || /\[.+\]/.test(s);
|
|
391
|
+
if (!hasWild) return null;
|
|
381
392
|
const segments = s.split('/').filter(Boolean);
|
|
382
393
|
const candidates = [s, ...segments];
|
|
383
394
|
for (const candidate of candidates) {
|
|
384
|
-
|
|
395
|
+
const candHasWild = candidate.includes('*') || candidate.includes('?') || /\[.+\]/.test(candidate);
|
|
396
|
+
if (!candHasWild) continue;
|
|
385
397
|
// Prefix match: ".antig*" → prefix ".antig"
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
398
|
+
const firstWild = Math.min(
|
|
399
|
+
candidate.includes('*') ? candidate.indexOf('*') : Infinity,
|
|
400
|
+
candidate.includes('?') ? candidate.indexOf('?') : Infinity,
|
|
401
|
+
/\[/.test(candidate) ? candidate.indexOf('[') : Infinity,
|
|
402
|
+
);
|
|
389
403
|
const prefix = candidate.slice(0, firstWild).toLowerCase();
|
|
390
404
|
if (prefix.length > 0) {
|
|
391
405
|
for (const p of protectedPaths) {
|
|
392
406
|
if (p.startsWith(prefix)) return p;
|
|
393
407
|
}
|
|
394
408
|
}
|
|
395
|
-
// Regex match
|
|
409
|
+
// Regex match — convert shell glob to regex
|
|
410
|
+
// Handles *, ?, and [...] character classes
|
|
396
411
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
412
|
+
let regex = '';
|
|
413
|
+
let i = 0;
|
|
414
|
+
const c = candidate.toLowerCase();
|
|
415
|
+
while (i < c.length) {
|
|
416
|
+
if (c[i] === '*') { regex += '.*'; i++; }
|
|
417
|
+
else if (c[i] === '?') { regex += '.'; i++; }
|
|
418
|
+
else if (c[i] === '[') {
|
|
419
|
+
// Pass through [...] as regex character class
|
|
420
|
+
const close = c.indexOf(']', i + 1);
|
|
421
|
+
if (close !== -1) {
|
|
422
|
+
regex += c.slice(i, close + 1);
|
|
423
|
+
i = close + 1;
|
|
424
|
+
} else {
|
|
425
|
+
regex += '\\['; i++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
regex += c[i].replace(/[.+^${}()|\\]/g, '\\$&');
|
|
430
|
+
i++;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const re = new RegExp('^' + regex + '$', 'i');
|
|
399
434
|
for (const p of protectedPaths) {
|
|
400
435
|
if (re.test(p)) return p;
|
|
401
436
|
}
|
|
@@ -452,10 +487,107 @@ process.stdin.on('end', async () => {
|
|
|
452
487
|
}
|
|
453
488
|
}
|
|
454
489
|
|
|
490
|
+
// ── Write-protection for .env, .gitignore ──
|
|
491
|
+
// These files can be READ but not written/deleted/moved
|
|
492
|
+
const toolNameLower = (data.tool_name || '').toLowerCase();
|
|
493
|
+
const isWriteTool = toolNameLower === 'write' || toolNameLower === 'edit' || toolNameLower === 'notebookedit';
|
|
494
|
+
const isBashTool = toolNameLower === 'bash';
|
|
495
|
+
const bashDestructive = /\b(?:rm|rmdir|del|unlink|mv|move|rename|cp|chmod|chown|truncate|shred)\b/i;
|
|
496
|
+
const fullCmd = rawStrings.join(' ');
|
|
497
|
+
|
|
498
|
+
for (const wp of writeProtectedPaths) {
|
|
499
|
+
if (isWriteTool) {
|
|
500
|
+
// Check file_path and all strings for write-protected paths
|
|
501
|
+
for (const s of allStrings) {
|
|
502
|
+
if (s.includes(wp)) {
|
|
503
|
+
await blockSelfProtection('SOLONGATE: Write to protected file "' + wp + '" is blocked');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (isBashTool && bashDestructive.test(fullCmd)) {
|
|
508
|
+
for (const s of allStrings) {
|
|
509
|
+
if (s.includes(wp)) {
|
|
510
|
+
await blockSelfProtection('SOLONGATE: Destructive operation on "' + wp + '" is blocked');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Also apply glob/wildcard checks for write-protected paths in destructive contexts
|
|
517
|
+
if (isBashTool && bashDestructive.test(fullCmd)) {
|
|
518
|
+
for (const s of allStrings) {
|
|
519
|
+
const hasWild = s.includes('*') || s.includes('?') || /\[.+\]/.test(s);
|
|
520
|
+
if (!hasWild) continue;
|
|
521
|
+
// Convert glob to regex and test against write-protected paths
|
|
522
|
+
try {
|
|
523
|
+
let regex = '';
|
|
524
|
+
let i = 0;
|
|
525
|
+
const c = s.toLowerCase();
|
|
526
|
+
while (i < c.length) {
|
|
527
|
+
if (c[i] === '*') { regex += '.*'; i++; }
|
|
528
|
+
else if (c[i] === '?') { regex += '.'; i++; }
|
|
529
|
+
else if (c[i] === '[') {
|
|
530
|
+
const close = c.indexOf(']', i + 1);
|
|
531
|
+
if (close !== -1) { regex += c.slice(i, close + 1); i = close + 1; }
|
|
532
|
+
else { regex += '\\['; i++; }
|
|
533
|
+
}
|
|
534
|
+
else { regex += c[i].replace(/[.+^${}()|\\]/g, '\\$&'); i++; }
|
|
535
|
+
}
|
|
536
|
+
const re = new RegExp('^' + regex + '$', 'i');
|
|
537
|
+
for (const wp of writeProtectedPaths) {
|
|
538
|
+
if (re.test(wp)) {
|
|
539
|
+
await blockSelfProtection('SOLONGATE: Wildcard "' + s + '" matches write-protected "' + wp + '" — blocked');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch {}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
455
546
|
// ── Layer 7: Block ALL inline code execution & dangerous patterns ──
|
|
456
547
|
// Runtime string construction (atob, Buffer.from, fromCharCode, array.join)
|
|
457
548
|
// makes static analysis impossible. Blanket-block these patterns.
|
|
458
|
-
|
|
549
|
+
|
|
550
|
+
// 7-pre. Blanket rule: destructive command + dot-prefixed glob = BLOCK
|
|
551
|
+
// Catches: rm -rf .[a-z]*, mv .[!.]* /tmp, etc.
|
|
552
|
+
// Any glob starting with "." combined with a destructive op is dangerous
|
|
553
|
+
const destructiveOps = /\b(?:rm|rmdir|del|unlink|mv|move|rename)\b/i;
|
|
554
|
+
const dotGlob = /\.\[|\.[\*\?]|\.{[^}]+}/; // .[a-z]*, .*, .?, .{foo,bar}
|
|
555
|
+
if (destructiveOps.test(fullCmd) && dotGlob.test(fullCmd)) {
|
|
556
|
+
await blockSelfProtection('SOLONGATE: Destructive command + dot-glob pattern — blocked');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 7-pre2. Heredoc to interpreter — BLOCK
|
|
560
|
+
// bash << 'EOF' / bash <<< "cmd" / sh << TAG
|
|
561
|
+
if (/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+<</i.test(fullCmd)) {
|
|
562
|
+
await blockSelfProtection('SOLONGATE: Heredoc to interpreter — blocked');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 7-pre3. Command substitution + destructive = BLOCK
|
|
566
|
+
// Catches: rm -rf $(node scan.mjs), rm `node scan.mjs`, rm $(bash scan.sh)
|
|
567
|
+
// Pattern: destructive command + $() or `` containing interpreter call
|
|
568
|
+
const hasDestructive = /\b(?:rm|rmdir|del|unlink|mv|move|rename|shred)\b/i.test(fullCmd);
|
|
569
|
+
const hasCmdSubInterpreter = /\$\(\s*(?:node|bash|sh|python[23]?|perl|ruby)\b/i.test(fullCmd)
|
|
570
|
+
|| /`\s*(?:node|bash|sh|python[23]?|perl|ruby)\b/i.test(fullCmd);
|
|
571
|
+
if (hasDestructive && hasCmdSubInterpreter) {
|
|
572
|
+
await blockSelfProtection('SOLONGATE: Command substitution + destructive op — blocked');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 7-pre4. Pipe from interpreter to destructive loop = BLOCK
|
|
576
|
+
// Catches: node scan.mjs | while read d; do rm -rf "$d"; done
|
|
577
|
+
// node scan.mjs | xargs rm -rf
|
|
578
|
+
// bash scan.sh | while read ...
|
|
579
|
+
const pipeFromInterpreter = /\b(?:node|bash|sh|python[23]?|perl|ruby)\s+\S+\s*\|/i;
|
|
580
|
+
if (pipeFromInterpreter.test(fullCmd) && hasDestructive) {
|
|
581
|
+
await blockSelfProtection('SOLONGATE: Pipe from script to destructive command — blocked');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 7-pre5. Script chaining: interpreter + destructive in same command chain
|
|
585
|
+
// Catches: node scan.mjs && rm -rf $(cat /tmp/targets.txt)
|
|
586
|
+
// bash scan.sh; while read d < targets.txt; do rm -rf "$d"; done
|
|
587
|
+
const hasScriptExec = /\b(?:node|bash|sh|python[23]?|perl|ruby)\s+\S+\.\S+/i.test(fullCmd);
|
|
588
|
+
if (hasScriptExec && hasDestructive) {
|
|
589
|
+
await blockSelfProtection('SOLONGATE: Script execution + destructive command in chain — blocked');
|
|
590
|
+
}
|
|
459
591
|
|
|
460
592
|
// 7a. Inline interpreter execution — TOTAL BLOCK (no content scan needed)
|
|
461
593
|
// These can construct ANY string at runtime, bypassing all static analysis
|
|
@@ -478,7 +610,8 @@ process.stdin.on('end', async () => {
|
|
|
478
610
|
|
|
479
611
|
// 7b. Pipe-to-interpreter — TOTAL BLOCK
|
|
480
612
|
// Any content piped to an interpreter can construct arbitrary commands at runtime
|
|
481
|
-
|
|
613
|
+
// Also catches: | xargs node, | xargs bash, etc.
|
|
614
|
+
const pipeToInterpreter = /\|\s*(?:xargs\s+)?(?:node|bash|sh|python[23]?|perl|ruby|php)\b/i;
|
|
482
615
|
if (pipeToInterpreter.test(fullCmd)) {
|
|
483
616
|
await blockSelfProtection('SOLONGATE: Pipe to interpreter blocked — runtime bypass risk');
|
|
484
617
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "MCP security proxy — protect any MCP server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|