@solongate/proxy 0.26.3 → 0.27.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 CHANGED
@@ -7,9 +7,19 @@
7
7
  * Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
8
8
  * Auto-installed by: npx @solongate/proxy init
9
9
  */
10
- import { readFileSync, existsSync } from 'node:fs';
10
+ import { readFileSync, existsSync, statSync } from 'node:fs';
11
11
  import { resolve } from 'node:path';
12
12
 
13
+ // Safe file read with size limit (1MB max) to prevent DoS via large files
14
+ const MAX_FILE_READ = 1024 * 1024; // 1MB
15
+ function safeReadFileSync(filePath, encoding = 'utf-8') {
16
+ try {
17
+ const stat = statSync(filePath);
18
+ if (stat.size > MAX_FILE_READ) return '';
19
+ return readFileSync(filePath, encoding);
20
+ } catch { return ''; }
21
+ }
22
+
13
23
  // ── Load API key from .env file (Claude Code doesn't load .env into process.env) ──
14
24
  function loadEnvKey(dir) {
15
25
  try {
@@ -157,6 +167,32 @@ function matchPathGlob(path, pattern) {
157
167
  return matchGlob(p, g);
158
168
  }
159
169
 
170
+ // ── Safe Webhook URL Validation (prevent SSRF) ──
171
+ function isSafeWebhookUrl(urlStr) {
172
+ try {
173
+ const u = new URL(urlStr);
174
+ if (u.protocol !== 'https:') return false;
175
+ const host = u.hostname.toLowerCase();
176
+ // Block private/reserved IPs and metadata endpoints
177
+ if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1') return false;
178
+ if (host.startsWith('10.') || host.startsWith('192.168.') || host.startsWith('172.')) return false;
179
+ if (host === '169.254.169.254' || host === 'metadata.google.internal') return false;
180
+ if (host.endsWith('.internal') || host.endsWith('.local')) return false;
181
+ return true;
182
+ } catch { return false; }
183
+ }
184
+
185
+ // ── Safe Regex Validation (prevent ReDoS from cloud-supplied patterns) ──
186
+ function isSafeRegex(pattern) {
187
+ if (typeof pattern !== 'string' || pattern.length > 512) return false;
188
+ // Block nested quantifiers: (a+)+, (a*)+, (a{1,})+, etc.
189
+ if (/(\+|\*|\{[^}]+\})\s*(\+|\*|\{[^}]+\})/.test(pattern)) return false;
190
+ if (/\([^)]*(\+|\*|\{[^}]+\})[^)]*\)\s*(\+|\*|\{[^}]+\})/.test(pattern)) return false;
191
+ // Block excessive alternation groups (>10 alternatives)
192
+ if ((pattern.match(/\|/g) || []).length > 10) return false;
193
+ try { new RegExp(pattern); return true; } catch { return false; }
194
+ }
195
+
160
196
  // ── Extract Functions (deep scan all string values) ──
161
197
  function scanStrings(obj) {
162
198
  const strings = [];
@@ -643,7 +679,7 @@ process.stdin.on('end', async () => {
643
679
  try {
644
680
  const targetPath = resolve(hookCwdForNpm, scriptFileMatch[1]);
645
681
  if (existsSync(targetPath)) {
646
- const targetContent = readFileSync(targetPath, 'utf-8').toLowerCase();
682
+ const targetContent = safeReadFileSync(targetPath).toLowerCase();
647
683
  // Check target file for protected paths
648
684
  for (const pp of [...protectedPaths, ...writeProtectedPaths]) {
649
685
  if (targetContent.includes(pp)) {
@@ -670,7 +706,7 @@ process.stdin.on('end', async () => {
670
706
  const candidates = [impAbs, impAbs + '.mjs', impAbs + '.js', impAbs + '.cjs'];
671
707
  for (const c of candidates) {
672
708
  if (existsSync(c)) {
673
- const impContent = readFileSync(c, 'utf-8').toLowerCase();
709
+ const impContent = safeReadFileSync(c).toLowerCase();
674
710
  const iDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bglob\b/i.test(impContent);
675
711
  const iDest = /\brmsync\b|\bunlinksync\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(impContent);
676
712
  if ((iDisc && tDest) || (tDisc && iDest)) {
@@ -756,7 +792,7 @@ process.stdin.on('end', async () => {
756
792
  ? scriptPath
757
793
  : resolve(hookCwdForScript, scriptPath);
758
794
  if (existsSync(absPath)) {
759
- const scriptContent = readFileSync(absPath, 'utf-8').toLowerCase();
795
+ const scriptContent = safeReadFileSync(absPath).toLowerCase();
760
796
  // Check for discovery+destruction combo
761
797
  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);
762
798
  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);
@@ -778,7 +814,7 @@ process.stdin.on('end', async () => {
778
814
  const candidates = [importAbs, importAbs + '.mjs', importAbs + '.js', importAbs + '.cjs'];
779
815
  for (const candidate of candidates) {
780
816
  if (existsSync(candidate)) {
781
- const importContent = readFileSync(candidate, 'utf-8').toLowerCase();
817
+ const importContent = safeReadFileSync(candidate).toLowerCase();
782
818
  // Cross-module: check if imported module has discovery/destruction/string construction
783
819
  const importDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b/i.test(importContent);
784
820
  const importDest = /\brmsync\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(importContent);
@@ -890,6 +926,7 @@ process.stdin.on('end', async () => {
890
926
  if (piCfg.piEnabled !== false && Array.isArray(piCfg.piWhitelist) && piCfg.piWhitelist.length > 0) {
891
927
  for (const wlPattern of piCfg.piWhitelist) {
892
928
  try {
929
+ if (!isSafeRegex(wlPattern)) continue;
893
930
  if (new RegExp(wlPattern, 'i').test(allText)) {
894
931
  whitelisted = true;
895
932
  break;
@@ -902,7 +939,7 @@ process.stdin.on('end', async () => {
902
939
  const customCategories = [];
903
940
  if (piCfg.piEnabled !== false && Array.isArray(piCfg.piCustomPatterns)) {
904
941
  for (const cp of piCfg.piCustomPatterns) {
905
- if (cp && cp.pattern) {
942
+ if (cp && cp.pattern && isSafeRegex(cp.pattern)) {
906
943
  try {
907
944
  customCategories.push({
908
945
  name: cp.name || 'custom_pattern',
@@ -950,8 +987,8 @@ process.stdin.on('end', async () => {
950
987
  });
951
988
  } catch {}
952
989
 
953
- // Webhook notification
954
- if (piCfg.piWebhookUrl) {
990
+ // Webhook notification (SSRF-safe: HTTPS only, no private IPs)
991
+ if (piCfg.piWebhookUrl && isSafeWebhookUrl(piCfg.piWebhookUrl)) {
955
992
  try {
956
993
  await fetch(piCfg.piWebhookUrl, {
957
994
  method: 'POST',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.26.3",
3
+ "version": "0.27.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": {
@@ -8,9 +8,12 @@
8
8
  "solongate-init": "./dist/init.js",
9
9
  "proxy": "./dist/index.js"
10
10
  },
11
- "main": "./dist/index.js",
11
+ "main": "./dist/lib.js",
12
12
  "exports": {
13
13
  ".": {
14
+ "import": "./dist/lib.js"
15
+ },
16
+ "./cli": {
14
17
  "import": "./dist/index.js"
15
18
  }
16
19
  },
@@ -61,7 +64,6 @@
61
64
  "devDependencies": {
62
65
  "@solongate/core": "workspace:*",
63
66
  "@solongate/policy-engine": "workspace:*",
64
- "@solongate/sdk": "workspace:*",
65
67
  "@solongate/tsconfig": "workspace:*",
66
68
  "tsup": "^8.3.0",
67
69
  "tsx": "^4.19.0",