@solongate/proxy 0.17.3 → 0.19.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.
Files changed (2) hide show
  1. package/hooks/guard.mjs +81 -42
  2. package/package.json +1 -1
package/hooks/guard.mjs CHANGED
@@ -452,61 +452,49 @@ process.stdin.on('end', async () => {
452
452
  }
453
453
  }
454
454
 
455
- // ── Layer 7: Dangerous execution pattern detection ──
456
- // These can construct ANY string at runtime block when touching protected dirs
455
+ // ── Layer 7: Block ALL inline code execution & dangerous patterns ──
456
+ // Runtime string construction (atob, Buffer.from, fromCharCode, array.join)
457
+ // makes static analysis impossible. Blanket-block these patterns.
457
458
  const fullCmd = rawStrings.join(' ');
458
459
 
459
- // 7a. Inline interpreter execution: node -e, python -c, perl -e, ruby -e
460
- // Extract the -e/-c argument and scan it
461
- const interpreterPatterns = [
462
- /\bnode\s+(?:-e|--eval)\s+["']([^"']+)["']/gi,
463
- /\bnode\s+(?:-e|--eval)\s+([^;&|"']+)/gi,
464
- /\bpython[23]?\s+-c\s+["']([^"']+)["']/gi,
465
- /\bperl\s+-e\s+["']([^"']+)["']/gi,
466
- /\bruby\s+-e\s+["']([^"']+)["']/gi,
460
+ // 7a. Inline interpreter execution TOTAL BLOCK (no content scan needed)
461
+ // These can construct ANY string at runtime, bypassing all static analysis
462
+ const blockedInterpreters = [
463
+ [/\bnode\s+(?:-e|--eval)\b/i, 'node -e/--eval'],
464
+ [/\bnode\s+-p\b/i, 'node -p'],
465
+ [/\bpython[23]?\s+-c\b/i, 'python -c'],
466
+ [/\bperl\s+-e\b/i, 'perl -e'],
467
+ [/\bruby\s+-e\b/i, 'ruby -e'],
468
+ [/\bpowershell(?:\.exe)?\s+.*-(?:EncodedCommand|e|ec)\b/i, 'powershell -EncodedCommand'],
469
+ [/\bpwsh(?:\.exe)?\s+.*-(?:EncodedCommand|e|ec)\b/i, 'pwsh -EncodedCommand'],
470
+ [/\bpowershell(?:\.exe)?\s+-c(?:ommand)?\b/i, 'powershell -Command'],
471
+ [/\bpwsh(?:\.exe)?\s+-c(?:ommand)?\b/i, 'pwsh -Command'],
467
472
  ];
468
- for (const pat of interpreterPatterns) {
469
- for (const m of fullCmd.matchAll(pat)) {
470
- const code = m[1].toLowerCase();
471
- for (const p of protectedPaths) {
472
- if (code.includes(p)) {
473
- await blockSelfProtection('SOLONGATE: Interpreter code targets "' + p + '" — blocked');
474
- }
475
- }
476
- // Also check the normalized version
477
- const normCode = normalizeShell(code);
478
- for (const p of protectedPaths) {
479
- if (normCode.includes(p)) {
480
- await blockSelfProtection('SOLONGATE: Interpreter code targets "' + p + '" — blocked');
481
- }
482
- }
473
+ for (const [pat, name] of blockedInterpreters) {
474
+ if (pat.test(fullCmd)) {
475
+ await blockSelfProtection('SOLONGATE: Inline code execution blocked (' + name + ')');
483
476
  }
484
477
  }
485
478
 
486
- // 7b. Base64 decode piped to execution always block
487
- if (/\bbase64\s+-d\b.*\|\s*(?:bash|sh|node|python|perl|ruby)\b/i.test(fullCmd) ||
488
- /\bbase64\s+--decode\b.*\|\s*(?:bash|sh|node|python|perl|ruby)\b/i.test(fullCmd)) {
489
- await blockSelfProtection('SOLONGATE: base64 decode piped to interpreter — blocked');
479
+ // 7b. Pipe-to-interpreterTOTAL BLOCK
480
+ // Any content piped to an interpreter can construct arbitrary commands at runtime
481
+ const pipeToInterpreter = /\|\s*(?:node|bash|sh|python[23]?|perl|ruby|php)\b/i;
482
+ if (pipeToInterpreter.test(fullCmd)) {
483
+ await blockSelfProtection('SOLONGATE: Pipe to interpreter blocked — runtime bypass risk');
490
484
  }
491
485
 
492
- // 7c. Temp script file execution: bash /path/file, sh /path/file
493
- // If "bash <file>" or "sh <file>" and the file is not a well-known script
494
- if (/\b(?:bash|sh)\s+(?:\/tmp\/|\/var\/tmp\/|~\/\.|\.\/[^.s])/i.test(fullCmd)) {
495
- await blockSelfProtection('SOLONGATE: Temp script execution detected — blocked');
486
+ // 7c. Base64 decode in ANY context block when piped to anything
487
+ if (/\bbase64\s+(?:-d|--decode)\b/i.test(fullCmd) && /\|/i.test(fullCmd)) {
488
+ await blockSelfProtection('SOLONGATE: base64 decode in pipe chain — blocked');
496
489
  }
497
490
 
498
- // 7d. Process substitution and here-strings that could construct protected paths
499
- if (/>\s*\(\s*(?:rm|mv|cp|cat)\b/i.test(fullCmd) || /<<<.*(?:rm|mv|cp|cat)\b/i.test(fullCmd)) {
500
- for (const p of protectedPaths) {
501
- const prefix = p.slice(0, 4); // e.g. ".sol", ".cla"
502
- if (fullCmd.includes(prefix)) {
503
- await blockSelfProtection('SOLONGATE: Process substitution near protected path "' + p + '" — blocked');
504
- }
505
- }
491
+ // 7d. Temp/arbitrary script file execution
492
+ if (/\b(?:bash|sh)\s+(?:\/tmp\/|\/var\/tmp\/|~\/|\/dev\/)/i.test(fullCmd)) {
493
+ await blockSelfProtection('SOLONGATE: Script execution from temp path — blocked');
506
494
  }
507
495
 
508
496
  // 7e. xargs with destructive operations
509
- if (/\bxargs\b.*\b(?:rm|mv|cp|rmdir|unlink)\b/i.test(fullCmd)) {
497
+ if (/\bxargs\b.*\b(?:rm|mv|cp|rmdir|unlink|del)\b/i.test(fullCmd)) {
510
498
  for (const p of protectedPaths) {
511
499
  if (fullCmd.includes(p.slice(0, 4))) {
512
500
  await blockSelfProtection('SOLONGATE: xargs with destructive op near "' + p + '" — blocked');
@@ -514,6 +502,57 @@ process.stdin.on('end', async () => {
514
502
  }
515
503
  }
516
504
 
505
+ // 7f. cmd.exe /c with encoded/constructed commands
506
+ if (/\bcmd(?:\.exe)?\s+\/c\b/i.test(fullCmd)) {
507
+ for (const p of protectedPaths) {
508
+ if (fullCmd.includes(p) || fullCmd.includes(p.slice(0, 4))) {
509
+ await blockSelfProtection('SOLONGATE: cmd.exe /c near protected path — blocked');
510
+ }
511
+ }
512
+ }
513
+
514
+ // 7g. Script file execution — scan file content for discovery+destruction combo
515
+ // Catches: bash script.sh / node script.mjs where the script uses readdirSync + rmSync
516
+ const scriptExecMatch = fullCmd.match(/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+([^\s;&|]+)/i);
517
+ if (scriptExecMatch) {
518
+ const scriptPath = scriptExecMatch[1];
519
+ try {
520
+ const hookCwdForScript = data.cwd || process.cwd();
521
+ const absPath = scriptPath.startsWith('/') || scriptPath.includes(':')
522
+ ? scriptPath
523
+ : resolve(hookCwdForScript, scriptPath);
524
+ if (existsSync(absPath)) {
525
+ const scriptContent = readFileSync(absPath, 'utf-8').toLowerCase();
526
+ // Check for discovery+destruction combo
527
+ const hasDiscovery = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b|\bls\s+-[adl]|\bls\s+\.\b|\bopendir\b|\bdir\.entries\b|\bwalkdir\b|\bls\b.*\.\[/.test(scriptContent);
528
+ const hasDestruction = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\s*\(|\brimraf\b|\bremovesync\b|\bremove_tree\b|\bshutil\.rmtree\b|\bwritefilesync\b|\bexecsync\b.*\brm\b|\bchild_process\b|\bfs\.\s*(?:rm|unlink|rmdir|write)/.test(scriptContent);
529
+ if (hasDiscovery && hasDestruction) {
530
+ await blockSelfProtection('SOLONGATE: Script contains directory discovery + destructive ops — blocked');
531
+ }
532
+ // Also check for protected path names in script content (existing check, now centralized)
533
+ for (const p of protectedPaths) {
534
+ if (scriptContent.includes(p)) {
535
+ await blockSelfProtection('SOLONGATE: Script references protected path "' + p + '" — blocked');
536
+ }
537
+ }
538
+ }
539
+ } catch {} // File read error — skip
540
+ }
541
+
542
+ // 7h. Write tool content scanning — detect discovery+destruction in file content being written
543
+ // Catches: Write tool creating a script that uses readdirSync('.') + rmSync
544
+ const toolName_ = data.tool_name || '';
545
+ if (toolName_.toLowerCase() === 'write' || toolName_.toLowerCase() === 'edit') {
546
+ const fileContent = (args.content || args.new_string || '').toLowerCase();
547
+ if (fileContent.length > 0) {
548
+ const hasDiscovery = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b|\bls\s+-[adl]|\bls\s+\.\b|\bopendir\b|\bdir\.entries\b|\bwalkdir\b|\bls\b.*\.\[/.test(fileContent);
549
+ const hasDestruction = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bremovesync\b|\bremove_tree\b|\bshutil\.rmtree\b|\bwritefilesync\b|\bexecsync\b.*\brm\b|\bchild_process\b.*\brm\b|\bfs\.\s*(?:rm|unlink|rmdir)/.test(fileContent);
550
+ if (hasDiscovery && hasDestruction) {
551
+ await blockSelfProtection('SOLONGATE: File content contains discovery + destructive ops — write blocked');
552
+ }
553
+ }
554
+ }
555
+
517
556
  // ── Fetch PI config from Cloud ──
518
557
  let piCfg = { piEnabled: true, piThreshold: 0.5, piMode: 'block', piWhitelist: [], piToolConfig: {}, piCustomPatterns: [], piWebhookUrl: null };
519
558
  if (API_KEY && API_KEY.startsWith('sg_live_')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.17.3",
3
+ "version": "0.19.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": {