beast-agent 2.30.6 → 2.32.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.30.6",
4
+ "version": "2.32.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ /* Dışa gönderim güvenlik kapısı: ajanın send_file aracı sır/anahtar
4
+ dosyalarını asla WhatsApp/sohbete göndermesin. Saf fonksiyon —
5
+ electron'a bağımlı değildir, test edilebilir. */
6
+
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ function norm(p) {
11
+ return String(p || '').replace(/\//g, '\\').toLowerCase();
12
+ }
13
+
14
+ /* true → bu dosya hassas, dışa gönderilmez */
15
+ function isSensitiveSendPath(abs, opts = {}) {
16
+ const appData = opts.appData || process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
17
+ const home = opts.home || os.homedir();
18
+ let full;
19
+ try {
20
+ full = norm(path.resolve(String(abs || '')));
21
+ } catch {
22
+ return false;
23
+ }
24
+ if (!full) return false;
25
+
26
+ /* Beast veri dizini: settings.json (API key'leri), wa-auth, mcp.json,
27
+ sessions, botlar, tools — tek komutla sızamaz */
28
+ const beastDir = norm(path.join(appData, 'beast'));
29
+ if (full === beastDir || full.startsWith(beastDir + '\\')) return true;
30
+
31
+ /* Kimlik/anahtar klasörleri */
32
+ for (const d of ['.ssh', '.aws', '.gnupg', '.docker', '.kube', '.config']) {
33
+ const p = norm(path.join(home, d));
34
+ if (full === p || full.startsWith(p + '\\')) return true;
35
+ }
36
+
37
+ const base = path.basename(full);
38
+ if (/^(id_rsa|id_ed25519|id_ecdsa|id_dsa|authorized_keys)(\.pub)?$/.test(base)) return true;
39
+ if (/^\.env(\.|$)/.test(base)) return true;
40
+ if (/\.(pem|key|pfx|p12|keystore|jks|ppk)$/.test(base)) return true;
41
+ if (/^(credentials|creds|secrets?)\.(json|ya?ml|txt)$/.test(base)) return true;
42
+ if (/^(\.npmrc|\.netrc|\.git-credentials|\.htpasswd)$/.test(base)) return true;
43
+ return false;
44
+ }
45
+
46
+ module.exports = { isSensitiveSendPath };
package/src/agent/mcp.js CHANGED
@@ -42,6 +42,23 @@ function configPath() {
42
42
 
43
43
  let cfgCache = { mtime: -1, cfg: { servers: {} } };
44
44
 
45
+ /* Sunucu kaydını normalize eder + token güvenliğini doğrular.
46
+ Komut yoksa null döner; satır sonu/NUL varsa fırlatır (kayıt atlanır/reddedilir). */
47
+ function normalizeServer(s) {
48
+ if (!s || typeof s !== 'object' || !s.command) return null;
49
+ const command = String(s.command);
50
+ assertSafeToken(command, 'MCP komutu');
51
+ const args = (Array.isArray(s.args) ? s.args : []).map((a) => assertSafeToken(a, 'MCP argümanı'));
52
+ return {
53
+ command,
54
+ args,
55
+ env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
56
+ enabled: s.enabled !== false,
57
+ tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
58
+ timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
59
+ };
60
+ }
61
+
45
62
  function readConfig(force) {
46
63
  try {
47
64
  const p = configPath();
@@ -51,15 +68,12 @@ function readConfig(force) {
51
68
  const cfg = { servers: {} };
52
69
  if (parsed && typeof parsed.servers === 'object') {
53
70
  for (const [name, s] of Object.entries(parsed.servers)) {
54
- if (!s || typeof s !== 'object' || !s.command) continue;
55
- cfg.servers[sanitizeName(name)] = {
56
- command: String(s.command),
57
- args: Array.isArray(s.args) ? s.args.map(String) : [],
58
- env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
59
- enabled: s.enabled !== false,
60
- tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
61
- timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
62
- };
71
+ try {
72
+ const srv = normalizeServer(s);
73
+ if (srv) cfg.servers[sanitizeName(name)] = srv;
74
+ } catch (e) {
75
+ log.warn(`[mcp] '${name}' geçersiz, atlandı: ${String((e && e.message) || e)}`);
76
+ }
63
77
  }
64
78
  }
65
79
  cfgCache = { mtime: st.mtimeMs, cfg };
@@ -76,9 +90,15 @@ function readConfig(force) {
76
90
 
77
91
  function saveConfig(cfg) {
78
92
  const p = configPath();
93
+ /* DİSKE YAZMADAN doğrula: güvensiz/yapıbozuk kayıt asla mcp.json'a düşmez */
94
+ const clean = { servers: {} };
95
+ for (const [name, s] of Object.entries((cfg && cfg.servers) || {})) {
96
+ const srv = normalizeServer(s);
97
+ if (srv) clean.servers[sanitizeName(name)] = srv;
98
+ }
79
99
  fs.mkdirSync(path.dirname(p), { recursive: true });
80
- fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
81
- cfgCache = { mtime: -1, cfg }; /* sonraki okuma diskten gelsin */
100
+ fs.writeFileSync(p, JSON.stringify(clean, null, 2) + '\n');
101
+ cfgCache = { mtime: -1, cfg: clean }; /* sonraki okuma diskten gelsin */
82
102
  }
83
103
 
84
104
  function sanitizeName(name) {
@@ -89,20 +109,77 @@ function sanitizeName(name) {
89
109
  .slice(0, 32) || 'server';
90
110
  }
91
111
 
112
+ /* UI'a maskeli ('***') giden env sırlarını diskteki mevcut değerlerle geri koyar.
113
+ Böylece kullanıcı formu kaydedince sırlar '***' ile ezilmez. */
114
+ function restoreMaskedSecrets(cfg) {
115
+ let cur;
116
+ try { cur = readConfig(true); } catch { return cfg; }
117
+ for (const [name, s] of Object.entries((cfg && cfg.servers) || {})) {
118
+ if (!s || typeof s !== 'object' || !s.env || typeof s.env !== 'object') continue;
119
+ const old = cur.servers[sanitizeName(name)];
120
+ if (!old || !old.env) continue;
121
+ for (const [k, v] of Object.entries(s.env)) {
122
+ if (String(v) === '***' && old.env[k] !== undefined) s.env[k] = old.env[k];
123
+ }
124
+ }
125
+ return cfg;
126
+ }
127
+
92
128
  /* ---------- süreç yaşam döngüsü ---------- */
93
129
 
130
+ /* Token doğrulama: satır sonu/NUL her platformda enjeksiyon vektörüdür */
131
+ function assertSafeToken(t, what) {
132
+ const s = String(t == null ? '' : t);
133
+ if (/[\0\r\n]/.test(s)) throw new Error(what + ' içinde satır sonu/NUL karakteri olamaz');
134
+ return s;
135
+ }
136
+
137
+ const CMD_META_RE = /[\s"&|<>^()]/;
138
+
139
+ /* KOMUT token'ı: yalnız gerekiyorsa tırnaklanır — aksi halde npm/npx gibi
140
+ .cmd sarmalayıcılarının %~dp0 çözümü bozulur. %/! reddedilir. */
141
+ function quoteCmdCommand(t) {
142
+ const s = assertSafeToken(t, 'MCP komutu');
143
+ if (/[%!]/.test(s)) throw new Error('MCP komutunda % veya ! kullanılamaz (güvenlik)');
144
+ if (!CMD_META_RE.test(s)) return s;
145
+ return '"' + s.replace(/"/g, '""') + '"';
146
+ }
147
+
148
+ /* ARGÜMAN token'ı: HER ZAMAN tırnak içinde → & | < > gibi metakarakterler
149
+ komut ayıramaz. %/! genişletilebildiği için reddedilir; kapanış tırnağı
150
+ öncesi ters bölü CRT kuralı gereği çiftlenir. */
151
+ function quoteCmdArg(t) {
152
+ const s = assertSafeToken(t, 'MCP argümanı');
153
+ if (/[%!]/.test(s)) throw new Error('MCP argümanında % veya ! kullanılamaz (güvenlik)');
154
+ const out = s.replace(/"/g, '""').replace(/(\\+)$/, '$1$1');
155
+ return '"' + out + '"';
156
+ }
157
+
94
158
  function spawnServer(cfg) {
95
- /* Windows: npx/uvx gibi .cmd sarmalayıcıları düz spawn ile başlamaz —
96
- cmd /d /s /c üzerinden, argümanlar elle tırnaklanarak geç. */
97
- const quote = (t) => (/[\s"]/u.test(t) ? '"' + t.replace(/"/g, '\\"') + '"' : t);
98
- const line = [cfg.command, ...cfg.args].map(quote).join(' ');
99
- const child = spawn(line, {
100
- shell: true,
159
+ const cmd = assertSafeToken(cfg.command, 'MCP komutu');
160
+ if (!cmd.trim()) throw new Error('MCP komutu boş');
161
+ const args = (Array.isArray(cfg.args) ? cfg.args : []).map((a) => assertSafeToken(a, 'MCP argümanı'));
162
+ const opts = {
101
163
  windowsHide: true,
102
164
  stdio: ['pipe', 'pipe', 'pipe'],
103
165
  env: { ...process.env, ...(cfg.env || {}) },
166
+ };
167
+ if (process.platform !== 'win32') {
168
+ /* POSIX: shell YOK — & | ; $() gibi metakarakterler enjekte edilemez */
169
+ return spawn(cmd, args, { ...opts, shell: false });
170
+ }
171
+ /* Windows: npx/uvx .cmd sarmalayıcıları düz spawn edilemez — cmd.exe'ye
172
+ tırnaklı/tırnaksız DOĞRU komut satırı elle kurulur; shell KAPALI,
173
+ windowsVerbatimArguments ile libuv müdahalesi kapatılır. */
174
+ const first = quoteCmdCommand(cmd);
175
+ const line = [first, ...args.map(quoteCmdArg)].join(' ');
176
+ /* İlk token tırnaklıysa cmd /s ilk ve son tırnağı KIRPAR — dış sarmal şart */
177
+ const full = first.startsWith('"') ? '"' + line + '"' : line;
178
+ return spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', full], {
179
+ ...opts,
180
+ shell: false,
181
+ windowsVerbatimArguments: true,
104
182
  });
105
- return child;
106
183
  }
107
184
 
108
185
  function connFor(name) {
@@ -414,6 +491,9 @@ module.exports = {
414
491
  configPath,
415
492
  readConfig,
416
493
  saveConfig,
494
+ restoreMaskedSecrets,
495
+ normalizeServer,
496
+ _spawnServer: spawnServer,
417
497
  mergeTools,
418
498
  toolSchemas,
419
499
  call,
@@ -4,6 +4,8 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const https = require('https');
7
+ const dns = require('dns');
8
+ const net = require('net');
7
9
  const { execFile } = require('child_process');
8
10
  const { spawn } = require('child_process');
9
11
  const research = require('./research');
@@ -877,6 +879,111 @@ function assertPublicHttpUrl(raw) {
877
879
  return u.toString();
878
880
  }
879
881
 
882
+ /* Özel/ayrılmış IP aralığı mı? (IPv4 + IPv6 + v4-mapped) */
883
+ function isPrivateIp(ip) {
884
+ const v = String(ip || '').toLowerCase().replace(/^\[|\]$/g, '');
885
+ if (v.includes(':')) {
886
+ if (v === '::1' || v === '::') return true;
887
+ if (/^f[cd]/.test(v)) return true; /* fc00::/7 unique-local */
888
+ if (/^fe[89ab]/.test(v)) return true; /* fe80::/10 link-local */
889
+ if (/^ff/.test(v)) return true; /* multicast */
890
+ if (v.startsWith('2001:db8')) return true; /* dokümantasyon */
891
+ const m = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); /* v4-mapped */
892
+ if (m) return isPrivateIp(m[1]);
893
+ return false;
894
+ }
895
+ const p = v.split('.').map(Number);
896
+ if (p.length !== 4 || p.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true; /* çözümlenemeyen → güvensiz */
897
+ if (p[0] === 0 || p[0] === 10 || p[0] === 127 || p[0] >= 224) return true;
898
+ if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true; /* CGNAT */
899
+ if (p[0] === 169 && p[1] === 254) return true; /* link-local */
900
+ if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true;
901
+ if (p[0] === 192 && (p[1] === 168 || (p[1] === 0 && (p[2] === 0 || p[2] === 2)) || (p[1] === 88 && p[2] === 99))) return true;
902
+ if (p[0] === 198 && (p[1] === 18 || p[1] === 19 || (p[1] === 51 && p[2] === 100))) return true;
903
+ if (p[0] === 203 && p[1] === 0 && p[2] === 113) return true;
904
+ return false;
905
+ }
906
+
907
+ /* DNS çözümlemesi: alan adı ÖZEL bir IP'ye çözülüyorsa (localtest.me vb.) engelle.
908
+ TOCTOU/DNS-rebinding için tam çözüm connection pinning gerektirir; bu katman
909
+ metin kontrolü + gerçek çözümleme + redirect doğrulamasıyla açığı kapatır. */
910
+ async function assertPublicHostResolved(hostname) {
911
+ const h = String(hostname || '').replace(/^\[|\]$/g, '');
912
+ if (!h) throw new Error('geçersiz adres');
913
+ if (net.isIP(h)) {
914
+ if (isPrivateIp(h)) throw new Error('yerel/ağ içi adreslere erişim engellendi');
915
+ return;
916
+ }
917
+ let addrs;
918
+ try {
919
+ addrs = await dns.promises.lookup(h, { all: true, verbatim: true });
920
+ } catch {
921
+ throw new Error('alan adı çözümlenemedi: ' + h);
922
+ }
923
+ if (!addrs || !addrs.length) throw new Error('alan adı çözümlenemedi: ' + h);
924
+ for (const a of addrs) {
925
+ if (isPrivateIp(a.address)) throw new Error('yerel/ağ içi adreslere erişim engellendi (' + h + ')');
926
+ }
927
+ }
928
+
929
+ /* http_fetch için: yönlendirmeleri ELLE takip eder ve HER adımı doğrular
930
+ (302 → 127.0.0.1 gibi bypass'ları kapatır). Döner: { res, url } */
931
+ async function fetchPublicFollow(rawUrl, opts = {}, timeoutMs = 30000, signal) {
932
+ let current = assertPublicHttpUrl(rawUrl);
933
+ for (let hop = 0; hop <= 5; hop++) {
934
+ const u = new URL(current);
935
+ await assertPublicHostResolved(u.hostname);
936
+ const ctrl = new AbortController();
937
+ const timer = setTimeout(() => ctrl.abort(new Error('zaman aşımı')), timeoutMs);
938
+ const onAbort = () => ctrl.abort(new Error('iptal'));
939
+ if (signal) {
940
+ if (signal.aborted) throw new Error('iptal');
941
+ signal.addEventListener('abort', onAbort, { once: true });
942
+ }
943
+ let res;
944
+ try {
945
+ res = await fetch(current, { ...opts, signal: ctrl.signal, redirect: 'manual' });
946
+ } finally {
947
+ clearTimeout(timer);
948
+ if (signal) signal.removeEventListener('abort', onAbort);
949
+ }
950
+ const loc = res.headers.get('location');
951
+ if (res.status >= 300 && res.status < 400 && loc) {
952
+ if (hop === 5) throw new Error('çok fazla yönlendirme');
953
+ current = assertPublicHttpUrl(new URL(loc, current).toString());
954
+ continue;
955
+ }
956
+ return { res, url: current };
957
+ }
958
+ throw new Error('çok fazla yönlendirme');
959
+ }
960
+
961
+ /* Gövdeyi SINIRLI oku — kötü niyetli sunucu sınırsız veri akıtıp belleği şişirmesin */
962
+ async function readTextCapped(res, byteCap = 1024 * 1024) {
963
+ if (!res.body || typeof res.body.getReader !== 'function') {
964
+ const t = await res.text();
965
+ return t.length > byteCap ? t.slice(0, byteCap) : t;
966
+ }
967
+ const reader = res.body.getReader();
968
+ const chunks = [];
969
+ let size = 0;
970
+ try {
971
+ while (true) {
972
+ const { done, value } = await reader.read();
973
+ if (done) break;
974
+ if (!value || !value.length) continue;
975
+ const room = byteCap - size;
976
+ if (room <= 0) break;
977
+ chunks.push(value.length > room ? value.slice(0, room) : value);
978
+ size += Math.min(value.length, room);
979
+ if (size >= byteCap) break;
980
+ }
981
+ } finally {
982
+ try { await reader.cancel(); } catch {}
983
+ }
984
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');
985
+ }
986
+
880
987
  const ENTITIES = {
881
988
  amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', mdash: '—', ndash: '–',
882
989
  hellip: '…', rsquo: '\u2019', lsquo: '\u2018', ldquo: '\u201c', rdquo: '\u201d',
@@ -1004,10 +1111,10 @@ async function webSearch(query, { maxResults = 8, signal } = {}) {
1004
1111
  const MAX_FETCH_CHARS = 9000;
1005
1112
 
1006
1113
  async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', timeoutMs = 30000, signal } = {}) {
1007
- const safe = assertPublicHttpUrl(url);
1008
1114
  const t = Math.min(Math.max(Number(timeoutMs) || 30000, 1000), 120000); // opencode: default 30s, max 120s
1009
- const res = await fetchWithTimeout(
1010
- safe,
1115
+ /* SSRF: metin + DNS çözümlemesi + her redirect adımı ayrı doğrulanır */
1116
+ const { res, url: safe } = await fetchPublicFollow(
1117
+ url,
1011
1118
  { headers: { 'User-Agent': UA, Accept: 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5' } },
1012
1119
  t,
1013
1120
  signal
@@ -1020,7 +1127,7 @@ async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', tim
1020
1127
  return { ok: true, url: safe, status: res.status, contentType: ctype, note: 'ikili/metin olmayan içerik indirilmedi' };
1021
1128
  }
1022
1129
  const cap = 400000;
1023
- let body = await res.text();
1130
+ let body = await readTextCapped(res, 1024 * 1024);
1024
1131
  if (body.length > cap) body = body.slice(0, cap);
1025
1132
  let content;
1026
1133
  if (/html/i.test(ctype)) {
@@ -2357,6 +2464,9 @@ module.exports = {
2357
2464
  globToRegExp,
2358
2465
  walkFiles,
2359
2466
  assertPublicHttpUrl,
2467
+ isPrivateIp,
2468
+ assertPublicHostResolved,
2469
+ fetchPublicFollow,
2360
2470
  parseDdgResults,
2361
2471
  htmlToText,
2362
2472
  webSearch,