@solongate/proxy 0.20.0 → 0.22.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/dist/index.js CHANGED
@@ -487,6 +487,30 @@ function unlockProtectedDirs() {
487
487
  } catch {
488
488
  }
489
489
  }
490
+ const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
491
+ for (const file of protectedFiles) {
492
+ const fullPath = resolve2(file);
493
+ if (!existsSync3(fullPath)) continue;
494
+ try {
495
+ if (process.platform === "win32") {
496
+ try {
497
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /remove:d '*S-1-1-0' /Q"`, { stdio: "ignore" });
498
+ } catch {
499
+ }
500
+ try {
501
+ execSync(`attrib -R "${fullPath}"`, { stdio: "ignore" });
502
+ } catch {
503
+ }
504
+ } else {
505
+ try {
506
+ execSync(`chattr -i "${fullPath}"`, { stdio: "ignore" });
507
+ } catch {
508
+ }
509
+ execSync(`chmod u+w "${fullPath}"`, { stdio: "ignore" });
510
+ }
511
+ } catch {
512
+ }
513
+ }
490
514
  }
491
515
  function installHooks(selectedTools = []) {
492
516
  unlockProtectedDirs();
@@ -577,7 +601,37 @@ function installHooks(selectedTools = []) {
577
601
  }
578
602
  }
579
603
  }
604
+ const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
605
+ for (const file of protectedFiles) {
606
+ const fullPath = resolve2(file);
607
+ if (existsSync3(fullPath)) {
608
+ if (process.platform === "win32") {
609
+ try {
610
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /remove:d '*S-1-1-0' /Q"`, { stdio: "ignore" });
611
+ } catch {
612
+ }
613
+ try {
614
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /deny '*S-1-1-0:(DE,WD,AD,WA)' /Q"`, { stdio: "ignore" });
615
+ } catch {
616
+ }
617
+ try {
618
+ execSync(`attrib +R "${fullPath}"`, { stdio: "ignore" });
619
+ } catch {
620
+ }
621
+ } else {
622
+ try {
623
+ execSync(`chmod a-w "${fullPath}"`, { stdio: "ignore" });
624
+ } catch {
625
+ }
626
+ try {
627
+ execSync(`chattr +i "${fullPath}"`, { stdio: "ignore" });
628
+ } catch {
629
+ }
630
+ }
631
+ }
632
+ }
580
633
  console.log(" OS-level DENY protection applied (icacls/chmod)");
634
+ console.log(" Protected files: .env, .gitignore, .mcp.json, policy.json");
581
635
  } catch {
582
636
  }
583
637
  console.log("");
package/dist/init.js CHANGED
@@ -194,6 +194,30 @@ function unlockProtectedDirs() {
194
194
  } catch {
195
195
  }
196
196
  }
197
+ const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
198
+ for (const file of protectedFiles) {
199
+ const fullPath = resolve(file);
200
+ if (!existsSync(fullPath)) continue;
201
+ try {
202
+ if (process.platform === "win32") {
203
+ try {
204
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /remove:d '*S-1-1-0' /Q"`, { stdio: "ignore" });
205
+ } catch {
206
+ }
207
+ try {
208
+ execSync(`attrib -R "${fullPath}"`, { stdio: "ignore" });
209
+ } catch {
210
+ }
211
+ } else {
212
+ try {
213
+ execSync(`chattr -i "${fullPath}"`, { stdio: "ignore" });
214
+ } catch {
215
+ }
216
+ execSync(`chmod u+w "${fullPath}"`, { stdio: "ignore" });
217
+ }
218
+ } catch {
219
+ }
220
+ }
197
221
  }
198
222
  function installHooks(selectedTools = []) {
199
223
  unlockProtectedDirs();
@@ -284,7 +308,37 @@ function installHooks(selectedTools = []) {
284
308
  }
285
309
  }
286
310
  }
311
+ const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
312
+ for (const file of protectedFiles) {
313
+ const fullPath = resolve(file);
314
+ if (existsSync(fullPath)) {
315
+ if (process.platform === "win32") {
316
+ try {
317
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /remove:d '*S-1-1-0' /Q"`, { stdio: "ignore" });
318
+ } catch {
319
+ }
320
+ try {
321
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /deny '*S-1-1-0:(DE,WD,AD,WA)' /Q"`, { stdio: "ignore" });
322
+ } catch {
323
+ }
324
+ try {
325
+ execSync(`attrib +R "${fullPath}"`, { stdio: "ignore" });
326
+ } catch {
327
+ }
328
+ } else {
329
+ try {
330
+ execSync(`chmod a-w "${fullPath}"`, { stdio: "ignore" });
331
+ } catch {
332
+ }
333
+ try {
334
+ execSync(`chattr +i "${fullPath}"`, { stdio: "ignore" });
335
+ } catch {
336
+ }
337
+ }
338
+ }
339
+ }
287
340
  console.log(" OS-level DENY protection applied (icacls/chmod)");
341
+ console.log(" Protected files: .env, .gitignore, .mcp.json, policy.json");
288
342
  } catch {
289
343
  }
290
344
  console.log("");
package/hooks/guard.mjs CHANGED
@@ -562,6 +562,134 @@ process.stdin.on('end', async () => {
562
562
  await blockSelfProtection('SOLONGATE: Heredoc to interpreter — blocked');
563
563
  }
564
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
+ }
591
+
592
+ // 7-pre6. Nested script substitution: node clean.mjs $(node scan.mjs)
593
+ // Two "harmless" scripts chained via command substitution — no rm in the command itself
594
+ const nestedScriptSub = /\b(?:node|python[23]?|perl|ruby|bash|sh)\s+\S+\.\S+\s+.*\$\(\s*(?:node|python[23]?|perl|ruby|bash|sh)\b/i;
595
+ const backtickNestedScript = /\b(?:node|python[23]?|perl|ruby|bash|sh)\s+\S+\.\S+\s+.*`\s*(?:node|python[23]?|perl|ruby|bash|sh)\b/i;
596
+ if (nestedScriptSub.test(fullCmd) || backtickNestedScript.test(fullCmd)) {
597
+ await blockSelfProtection('SOLONGATE: Nested script substitution — blocked (script output as args to another script)');
598
+ }
599
+
600
+ // 7-pre7. NODE_OPTIONS injection — block commands with NODE_OPTIONS env var
601
+ // NODE_OPTIONS="--require malicious.cjs" can inject code into any node process
602
+ if (/\bNODE_OPTIONS\s*=/i.test(fullCmd)) {
603
+ await blockSelfProtection('SOLONGATE: NODE_OPTIONS injection — blocked');
604
+ }
605
+
606
+ // 7-pre8. npm lifecycle scripts — scan package.json before npm install/run
607
+ // npm install can trigger postinstall/preinstall/prepare scripts
608
+ if (/\bnpm\s+(?:install|i|ci|run|start|test|publish|pack)\b/i.test(fullCmd) || /\bnpx\s+/i.test(fullCmd)) {
609
+ try {
610
+ const hookCwdForNpm = data.cwd || process.cwd();
611
+ const pkgPath = resolve(hookCwdForNpm, 'package.json');
612
+ if (existsSync(pkgPath)) {
613
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
614
+ const scripts = pkg.scripts || {};
615
+ const lifecycleKeys = ['preinstall', 'install', 'postinstall', 'prepare', 'prepublish', 'prepublishOnly', 'prepack', 'postpack'];
616
+ const allScripts = Object.entries(scripts);
617
+ for (const [key, val] of allScripts) {
618
+ if (typeof val !== 'string') continue;
619
+ const scriptLower = val.toLowerCase();
620
+ // Check lifecycle scripts for dangerous patterns
621
+ const isLifecycle = lifecycleKeys.includes(key);
622
+ const isExplicitRun = fullCmd.includes(key); // npm run <key>
623
+ if (isLifecycle || isExplicitRun) {
624
+ // Check for protected path names
625
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
626
+ if (scriptLower.includes(p)) {
627
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" references protected "' + p + '" — blocked');
628
+ }
629
+ }
630
+ // Check for string construction patterns
631
+ if (/fromcharcode|atob\s*\(|buffer\.from|\\x[0-9a-f]{2}/i.test(scriptLower)) {
632
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" contains string construction — blocked');
633
+ }
634
+ // Check for discovery+destruction combo
635
+ const hasDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob\b|\bls\s+-[adl]/i.test(scriptLower);
636
+ const hasDest = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b/i.test(scriptLower);
637
+ if (hasDisc && hasDest) {
638
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" has discovery+destruction — blocked');
639
+ }
640
+ // Follow node/bash <file> references in npm scripts — deep scan the target file
641
+ const scriptFileMatch = scriptLower.match(/\b(?:node|bash|sh|python[23]?)\s+([^\s;&|$]+)/i);
642
+ if (scriptFileMatch) {
643
+ try {
644
+ const targetPath = resolve(hookCwdForNpm, scriptFileMatch[1]);
645
+ if (existsSync(targetPath)) {
646
+ const targetContent = readFileSync(targetPath, 'utf-8').toLowerCase();
647
+ // Check target file for protected paths
648
+ for (const pp of [...protectedPaths, ...writeProtectedPaths]) {
649
+ if (targetContent.includes(pp)) {
650
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" runs file referencing "' + pp + '" — blocked');
651
+ }
652
+ }
653
+ // Check target for discovery+destruction
654
+ const tDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bglob\b/i.test(targetContent);
655
+ const tDest = /\brmsync\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(targetContent);
656
+ if (tDisc && tDest) {
657
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" runs file with discovery+destruction — blocked');
658
+ }
659
+ // Check target for string construction + destruction
660
+ const tStrCon = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b/i.test(targetContent);
661
+ if (tStrCon && tDest) {
662
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" runs file with string construction+destruction — blocked');
663
+ }
664
+ // Follow imports in the target file
665
+ const tDir = targetPath.replace(/[/\\][^/\\]+$/, '');
666
+ const tImports = [...targetContent.matchAll(/(?:import\s+.*?\s+from\s+|import\s+|require\s*\()['"]\.\/([^'"]+)['"]/gi)];
667
+ for (const [, imp] of tImports) {
668
+ try {
669
+ const impAbs = resolve(tDir, imp);
670
+ const candidates = [impAbs, impAbs + '.mjs', impAbs + '.js', impAbs + '.cjs'];
671
+ for (const c of candidates) {
672
+ if (existsSync(c)) {
673
+ const impContent = readFileSync(c, 'utf-8').toLowerCase();
674
+ const iDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bglob\b/i.test(impContent);
675
+ const iDest = /\brmsync\b|\bunlinksync\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(impContent);
676
+ if ((iDisc && tDest) || (tDisc && iDest)) {
677
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" — cross-module discovery+destruction — blocked');
678
+ }
679
+ break;
680
+ }
681
+ }
682
+ } catch {}
683
+ }
684
+ }
685
+ } catch {}
686
+ }
687
+ }
688
+ }
689
+ }
690
+ } catch {} // Package.json read error — skip
691
+ }
692
+
565
693
  // 7a. Inline interpreter execution — TOTAL BLOCK (no content scan needed)
566
694
  // These can construct ANY string at runtime, bypassing all static analysis
567
695
  const blockedInterpreters = [
@@ -583,7 +711,8 @@ process.stdin.on('end', async () => {
583
711
 
584
712
  // 7b. Pipe-to-interpreter — TOTAL BLOCK
585
713
  // Any content piped to an interpreter can construct arbitrary commands at runtime
586
- const pipeToInterpreter = /\|\s*(?:node|bash|sh|python[23]?|perl|ruby|php)\b/i;
714
+ // Also catches: | xargs node, | xargs bash, etc.
715
+ const pipeToInterpreter = /\|\s*(?:xargs\s+)?(?:node|bash|sh|python[23]?|perl|ruby|php)\b/i;
587
716
  if (pipeToInterpreter.test(fullCmd)) {
588
717
  await blockSelfProtection('SOLONGATE: Pipe to interpreter blocked — runtime bypass risk');
589
718
  }
@@ -618,7 +747,7 @@ process.stdin.on('end', async () => {
618
747
 
619
748
  // 7g. Script file execution — scan file content for discovery+destruction combo
620
749
  // Catches: bash script.sh / node script.mjs where the script uses readdirSync + rmSync
621
- const scriptExecMatch = fullCmd.match(/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+([^\s;&|]+)/i);
750
+ const scriptExecMatch = fullCmd.match(/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+([^\s;&|$`]+)/i);
622
751
  if (scriptExecMatch) {
623
752
  const scriptPath = scriptExecMatch[1];
624
753
  try {
@@ -630,12 +759,50 @@ process.stdin.on('end', async () => {
630
759
  const scriptContent = readFileSync(absPath, 'utf-8').toLowerCase();
631
760
  // Check for discovery+destruction combo
632
761
  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);
633
- 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);
762
+ 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|\bfs\.\s*(?:rm|unlink|rmdir|write)/.test(scriptContent);
634
763
  if (hasDiscovery && hasDestruction) {
635
764
  await blockSelfProtection('SOLONGATE: Script contains directory discovery + destructive ops — blocked');
636
765
  }
637
- // Also check for protected path names in script content (existing check, now centralized)
638
- for (const p of protectedPaths) {
766
+ // String construction + destructive = BLOCK
767
+ // Runtime string construction (fromCharCode, atob, Buffer.from) bypasses literal name checks
768
+ const hasStringConstruction = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b|\\x[0-9a-f]{2}|\bstring\.fromcodepoin/i.test(scriptContent);
769
+ if (hasStringConstruction && hasDestruction) {
770
+ await blockSelfProtection('SOLONGATE: Script uses string construction + destructive ops — blocked');
771
+ }
772
+ // Import/require chain: if script imports other local files, scan them too
773
+ const scriptDir = absPath.replace(/[/\\][^/\\]+$/, '');
774
+ const imports = [...scriptContent.matchAll(/(?:import\s+.*?\s+from\s+|import\s+|require\s*\()['"]\.\/([^'"]+)['"]/gi)];
775
+ for (const [, importPath] of imports) {
776
+ try {
777
+ const importAbs = resolve(scriptDir, importPath);
778
+ const candidates = [importAbs, importAbs + '.mjs', importAbs + '.js', importAbs + '.cjs'];
779
+ for (const candidate of candidates) {
780
+ if (existsSync(candidate)) {
781
+ const importContent = readFileSync(candidate, 'utf-8').toLowerCase();
782
+ // Cross-module: check if imported module has discovery/destruction/string construction
783
+ const importDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b/i.test(importContent);
784
+ const importDest = /\brmsync\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(importContent);
785
+ const importStrCon = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b/i.test(importContent);
786
+ // Cross-module discovery in import + destruction in main (or vice versa)
787
+ if ((importDisc && hasDestruction) || (hasDiscovery && importDest)) {
788
+ await blockSelfProtection('SOLONGATE: Cross-module discovery+destruction detected — blocked');
789
+ }
790
+ if ((importStrCon && hasDestruction) || (hasStringConstruction && importDest)) {
791
+ await blockSelfProtection('SOLONGATE: Cross-module string construction+destruction — blocked');
792
+ }
793
+ // Check imported module for protected paths
794
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
795
+ if (importContent.includes(p)) {
796
+ await blockSelfProtection('SOLONGATE: Imported module references protected "' + p + '" — blocked');
797
+ }
798
+ }
799
+ break;
800
+ }
801
+ }
802
+ } catch {}
803
+ }
804
+ // Check for protected path names in script content
805
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
639
806
  if (scriptContent.includes(p)) {
640
807
  await blockSelfProtection('SOLONGATE: Script references protected path "' + p + '" — blocked');
641
808
  }
@@ -655,6 +822,39 @@ process.stdin.on('end', async () => {
655
822
  if (hasDiscovery && hasDestruction) {
656
823
  await blockSelfProtection('SOLONGATE: File content contains discovery + destructive ops — write blocked');
657
824
  }
825
+ // String construction + destructive = BLOCK
826
+ const hasStringConstruction = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b|\\x[0-9a-f]{2}|\bstring\.fromcodepoin/i.test(fileContent);
827
+ if (hasStringConstruction && hasDestruction) {
828
+ await blockSelfProtection('SOLONGATE: File uses string construction + destructive ops — write blocked');
829
+ }
830
+ }
831
+ }
832
+
833
+ // 7i. Write tool — detect package.json scripts with dangerous patterns
834
+ if (toolName_.toLowerCase() === 'write' || toolName_.toLowerCase() === 'edit') {
835
+ const filePath = (args.file_path || '').toLowerCase();
836
+ if (filePath.endsWith('package.json')) {
837
+ const content = args.content || args.new_string || '';
838
+ try {
839
+ // Try parsing as JSON to check scripts
840
+ const pkg = JSON.parse(content);
841
+ const scripts = pkg.scripts || {};
842
+ for (const [key, val] of Object.entries(scripts)) {
843
+ if (typeof val !== 'string') continue;
844
+ const v = val.toLowerCase();
845
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
846
+ if (v.includes(p)) {
847
+ await blockSelfProtection('SOLONGATE: package.json script "' + key + '" references protected "' + p + '" — blocked');
848
+ }
849
+ }
850
+ if (/fromcharcode|atob\s*\(|buffer\.from/i.test(v)) {
851
+ const hasDest = /\brmsync\b|\brm\b|\bunlink\b|\brimraf\b/i.test(v);
852
+ if (hasDest) {
853
+ await blockSelfProtection('SOLONGATE: package.json script "' + key + '" has string construction + destruction — blocked');
854
+ }
855
+ }
856
+ }
857
+ } catch {} // Not valid JSON or parse error — skip
658
858
  }
659
859
  }
660
860
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.20.0",
3
+ "version": "0.22.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": {