@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/dist/create.js +2 -2
- package/dist/index.js +2424 -3044
- package/dist/init.js +3 -2
- package/dist/inject.js +11 -11
- package/dist/lib.js +4968 -0
- package/hooks/guard.mjs +45 -8
- package/package.json +5 -3
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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/
|
|
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",
|