natureco-cli 5.42.0 → 5.43.1

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.
@@ -0,0 +1,40 @@
1
+ # Security Audit Summary — natureco-cli
2
+
3
+ **Denetim:** 3 turluk güvenlik incelemesi
4
+ **Bulgular:** 9 gerçek, kanıtlanmış açık
5
+ **Düzeltme tarihi:** 2026-07-08 (sürüm 5.43.0)
6
+ **Doğrulama:** 567 test yeşil (+29 güvenlik regresyonu), ESLint temiz, `npm run smoke` geçti
7
+
8
+ Her bulgu için kırmızı→yeşil regresyon testi eklendi. Aşağıda önem sırasına göre listelenmiştir.
9
+
10
+ | # | Önem | Bulgu | Dosya(lar) | Durum | Test |
11
+ |---|------|-------|-----------|-------|------|
12
+ | 1 | Kritik/RCE | shell_command onay/güvenlik akışını atlıyordu | `src/tools/shell_command.js`, `src/utils/tool-runner.js` | ✅ Düzeltildi | `security-hardening.test.js` |
13
+ | 2 | Yüksek | isSafeCommand prefix bypass (`echo hi; rm -rf ~`) | `src/utils/approvals.js` | ✅ Düzeltildi | `security-hardening.test.js` |
14
+ | 3 | Orta | config.json 0644 (dünya-okunabilir API key) | `src/utils/config.js` | ✅ Düzeltildi | `security-permissions.test.js` |
15
+ | 4 | Orta | WhatsApp session dizini zayıf izinler | `src/commands/gateway-server.js` | ✅ Düzeltildi | (izin kontrolü, POSIX) |
16
+ | 5 | Düşük | document_extract + pgrep/adb shell injection | `document_extract.js`, `social_open.js`, `youtube_ac.js`, `phone_control_enhanced.js` | ✅ Düzeltildi | `security-permissions.test.js` |
17
+ | 6 | Kritik/RCE | Skill indirme (keyfi repo) → prompt injection zinciri | `src/tools/skills_download.js`, `src/tools/skills_autoload.js` | ✅ Düzeltildi | `skills-download-security.test.js` |
18
+ | 7 | Yüksek | Kanal gönderen doğrulaması yok + hafıza sızıntısı | `src/commands/gateway-server.js` | ✅ Düzeltildi | `channel-gate-security.test.js` |
19
+ | 8 | Kritik | admin-rpc auth'suz + 0.0.0.0 dinliyor | `src/commands/admin-rpc.js` | ✅ Düzeltildi | `admin-rpc-security.test.js` |
20
+ | 9 | Kritik/persistence | cron_create sistem crontab'ına kontrolsüz yazıyor | `src/tools/cron_create.js` | ✅ Düzeltildi | `cron-security.test.js` |
21
+
22
+ ## Düzeltme özetleri
23
+
24
+ **1 + 6 (RCE zinciri):** Madde 6 (keyfi skill indirme → system-prompt enjeksiyonu) ile madde 1 (`shell_command` bypass) birlikte tam bir uzaktan kod çalıştırma zinciri oluşturuyordu. İkisi de kapatıldı: skill indirme yalnızca `KNOWN_REPOS` + kullanıcı onaylı allowlist ile; `shell_command` artık `bash.js` ile aynı `checkCommand`/`isDangerousCommand`/onay akışından geçiyor.
25
+
26
+ **2:** `isSafeCommand` artık shell metakarakteri (`; && | \` $() > …`) içeren hiçbir komutu "safe" saymıyor; prefix eşleşmesi kelime sınırında; `node -e` (inline eval) güvenli listeden çıkarıldı.
27
+
28
+ **3 + 4:** Hassas dosya/dizinler (config.json + API key yedekleri, WhatsApp oturum dosyaları) artık `0600`/`0700` ile, eski kurulumlar için `chmod` fallback ile korunuyor.
29
+
30
+ **5:** Kullanıcı/model girdisi içeren tüm `execSync(\`...${...}\`)` desenleri `execFileSync` (shell yok) veya tırnak-farkındalıklı tokenizer'a çevrildi.
31
+
32
+ **7:** Ortak `channelGate` helper'ı — allow-list kuruluysa yetkisiz göndereni engeller; kurulu değilse yanıt verir ama kişisel hafızayı system prompt'a **enjekte etmez** (anonim kanaldan hafıza sızıntısını önler).
33
+
34
+ **8:** admin-rpc artık `127.0.0.1`'e bind (opsiyonel `--expose` ile 0.0.0.0, token yine zorunlu) + her istekte `Authorization: Bearer <token>` (`~/.natureco/admin-token`, 0600) + `config.get` yanıtında secret maskeleme (`reveal:true` ile açılır).
35
+
36
+ **9:** cron_create tehlikeli komutu reddeder; sistem crontab'ına yazma varsayılan olarak kapalı (schema'da parametre yok → ajan tetikleyemez), yalnızca uygulama-içi `crons.json`'a yazar (natureco daemon kontrollü çalıştırır).
37
+
38
+ ## Kalan not (defense-in-depth)
39
+ - Madde 6'da ana vektör (keyfi repo indirme) kapatıldı. Elle `~/.natureco/skills/` altına konan skill'ler için per-skill ilk-kullanım onayı gelecek bir iyileştirme olarak değerlendirilebilir; şimdilik autoload ham içerik enjeksiyonu kaldırıldı.
40
+ - `--expose` ile admin-rpc'yi dışa açmak yalnızca güvenilir ağda ve token ile önerilir.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.42.0",
3
+ "version": "5.43.1",
4
4
  "description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -13,6 +13,7 @@
13
13
  "README.md",
14
14
  "CHANGELOG.md",
15
15
  "AUDIT.md",
16
+ "SECURITY_AUDIT_SUMMARY.md",
16
17
  "DEPLOY_v2.0.0.md"
17
18
  ],
18
19
  "scripts": {
@@ -15,11 +15,60 @@ const ALLOWED_METHODS = [
15
15
 
16
16
  let serverInstance = null;
17
17
 
18
+ // v5.43 GÜVENLİK: RPC sunucusu kimlik doğrulamasız ve 0.0.0.0'da dinliyordu → config.get
19
+ // ile tüm API key'ler ağdan okunabilir, config.set ile providerUrl kaçırılabilirdi.
20
+ // Zorunlu bearer token + localhost bind + secret maskeleme.
21
+ const SENSITIVE_KEY_RE = /(api[-_]?key|token|secret|password|passwd|credential|authorization|bearer|access[-_]?key|private[-_]?key)/i;
22
+
23
+ function maskSecrets(obj, reveal) {
24
+ if (reveal) return obj;
25
+ const walk = (o) => {
26
+ if (o === null || typeof o !== 'object') return o;
27
+ if (Array.isArray(o)) return o.map(walk);
28
+ const out = {};
29
+ for (const [k, v] of Object.entries(o)) {
30
+ if (SENSITIVE_KEY_RE.test(k) && typeof v === 'string' && v) {
31
+ out[k] = v.length > 8 ? `${v.slice(0, 3)}****${v.slice(-3)}` : '****';
32
+ } else {
33
+ out[k] = walk(v);
34
+ }
35
+ }
36
+ return out;
37
+ };
38
+ return walk(obj);
39
+ }
40
+
41
+ function getOrCreateAdminToken() {
42
+ const fs = require('fs');
43
+ const path = require('path');
44
+ const os = require('os');
45
+ const crypto = require('crypto');
46
+ const dir = path.join(os.homedir(), '.natureco');
47
+ const tokenFile = path.join(dir, 'admin-token');
48
+ try {
49
+ if (fs.existsSync(tokenFile)) {
50
+ const t = fs.readFileSync(tokenFile, 'utf8').trim();
51
+ if (t) { try { fs.chmodSync(tokenFile, 0o600); } catch {} return t; }
52
+ }
53
+ } catch {}
54
+ const token = crypto.randomBytes(32).toString('hex');
55
+ try {
56
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
57
+ fs.writeFileSync(tokenFile, token, { mode: 0o600 });
58
+ fs.chmodSync(tokenFile, 0o600);
59
+ } catch {}
60
+ return token;
61
+ }
62
+
18
63
  function adminRpc(args) {
19
64
  const [action, ...params] = args || [];
20
65
 
21
66
  if (!action || action === 'status') return statusAdmin();
22
- if (action === 'start') return startAdmin(params[0]);
67
+ if (action === 'start') {
68
+ const expose = params.includes('--expose');
69
+ const portArg = params.find(p => /^\d+$/.test(p));
70
+ return startAdmin(portArg, { expose });
71
+ }
23
72
  if (action === 'stop') return stopAdmin();
24
73
  if (action === 'call') return callMethod(params[0], params.slice(1).join(' '));
25
74
  if (action === 'methods') return listMethods();
@@ -103,11 +152,13 @@ async function callMethod(method, jsonParams) {
103
152
  if (method === 'config.get') {
104
153
  const config = getConfig();
105
154
  const key = params.key;
155
+ const reveal = params.reveal === true;
106
156
  if (key) {
107
157
  const value = key.split('.').reduce((o, k) => o?.[k], config);
108
- console.log(chalk.white(` ${key}: `) + chalk.cyan(JSON.stringify(value)));
158
+ const masked = maskSecrets({ [key]: value }, reveal);
159
+ console.log(chalk.white(` ${key}: `) + chalk.cyan(JSON.stringify(masked[key])));
109
160
  } else {
110
- console.log(chalk.cyan(JSON.stringify(config, null, 2)));
161
+ console.log(chalk.cyan(JSON.stringify(maskSecrets(config, reveal), null, 2)));
111
162
  }
112
163
  console.log();
113
164
  return;
@@ -198,13 +249,17 @@ async function callMethod(method, jsonParams) {
198
249
  console.log(chalk.yellow(` ⚠️ Method "${method}" henüz implemente edilmedi\n`));
199
250
  }
200
251
 
201
- function startAdmin(portStr) {
252
+ function startAdmin(portStr, opts = {}) {
202
253
  if (serverInstance) {
203
254
  console.log(chalk.yellow('\n ⚠️ Server zaten çalışıyor\n'));
204
255
  return;
205
256
  }
206
257
 
207
258
  const port = parseInt(portStr, 10) || 3847;
259
+ // Varsayılan: yalnızca localhost. --expose ile 0.0.0.0 (token yine ZORUNLU).
260
+ const expose = opts.expose === true;
261
+ const host = expose ? '0.0.0.0' : '127.0.0.1';
262
+ const adminToken = getOrCreateAdminToken();
208
263
 
209
264
  const server = http.createServer((req, res) => {
210
265
  if (req.method !== 'POST') {
@@ -213,6 +268,14 @@ function startAdmin(portStr) {
213
268
  return;
214
269
  }
215
270
 
271
+ // v5.43 GÜVENLİK: zorunlu bearer token — eşleşmezse 401 (body okumadan reddet).
272
+ const auth = req.headers['authorization'] || '';
273
+ if (auth !== `Bearer ${adminToken}`) {
274
+ res.writeHead(401, { 'Content-Type': 'application/json' });
275
+ res.end(JSON.stringify({ ok: false, error: 'Unauthorized: Authorization: Bearer <token> gerekli (~/.natureco/admin-token)' }));
276
+ return;
277
+ }
278
+
216
279
  let body = '';
217
280
  req.on('data', chunk => { body += chunk; });
218
281
  req.on('end', async () => {
@@ -239,7 +302,9 @@ function startAdmin(portStr) {
239
302
  payload = { gateway: config.gatewayUrl, provider: config.provider, model: config.model };
240
303
  } else if (rpc.method === 'config.get') {
241
304
  const key = rpc.params?.key;
242
- payload = key ? { [key]: key.split('.').reduce((o, k) => o?.[k], config) } : config;
305
+ const raw = key ? { [key]: key.split('.').reduce((o, k) => o?.[k], config) } : config;
306
+ // v5.43 GÜVENLİK: hassas alanları maskele; tam değer için params.reveal:true gerekir.
307
+ payload = maskSecrets(raw, rpc.params?.reveal === true);
243
308
  } else if (rpc.method === 'plugins.list') {
244
309
  const { loadTools } = require('../utils/tool-runner');
245
310
  payload = { tools: Object.keys(loadTools()) };
@@ -273,11 +338,15 @@ function startAdmin(portStr) {
273
338
  serverInstance = server;
274
339
  serverInstance.port = port;
275
340
 
276
- server.listen(port, () => {
277
- console.log(chalk.green(`\n ✅ Admin RPC server started on http://localhost:${port}\n`));
278
- console.log(chalk.gray(' POST requests with JSON body:'));
279
- console.log(chalk.white(' { "method": "health", "id": "req-1" }'));
280
- console.log(chalk.white(' { "method": "config.get", "params": { "key": "provider" } }'));
341
+ server.listen(port, host, () => {
342
+ console.log(chalk.green(`\n ✅ Admin RPC server started on http://${host}:${port}\n`));
343
+ if (expose) {
344
+ console.log(chalk.red(' ⚠️ --expose ile TÜM arayüzlerinde (0.0.0.0) dinliyor. Bearer token ZORUNLU; yine de yalnızca güvendiğin ağda kullan.'));
345
+ }
346
+ console.log(chalk.gray(' Bearer token (~/.natureco/admin-token):'));
347
+ console.log(chalk.white(` ${adminToken}`));
348
+ console.log(chalk.gray(' Örnek istek:'));
349
+ console.log(chalk.white(` curl -H "Authorization: Bearer ${adminToken.slice(0, 8)}..." -d '{"method":"health"}' http://127.0.0.1:${port}`));
281
350
  console.log(chalk.gray('\n Press Ctrl+C to stop\n'));
282
351
  });
283
352
 
@@ -300,3 +369,8 @@ function stopAdmin() {
300
369
  }
301
370
 
302
371
  module.exports = adminRpc;
372
+ // v5.43: test için — auth/bind/maskeleme regresyonu
373
+ module.exports.maskSecrets = maskSecrets;
374
+ module.exports.getOrCreateAdminToken = getOrCreateAdminToken;
375
+ module.exports.startAdmin = startAdmin;
376
+ module.exports.stopAdmin = stopAdmin;
@@ -301,6 +301,15 @@ async function runGatewayWorker() {
301
301
 
302
302
  async function startWhatsAppProvider(sessionDir, config) {
303
303
  try {
304
+ // v5.43 GÜVENLİK: session dizini Baileys kimlik/oturum dosyalarını tutar; çalınırsa
305
+ // WhatsApp hesabı ele geçirilebilir → ssh anahtarları gibi 0700 (parent dahil).
306
+ try {
307
+ const parent = path.dirname(sessionDir);
308
+ if (!fs.existsSync(parent)) fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
309
+ else fs.chmodSync(parent, 0o700);
310
+ if (!fs.existsSync(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
311
+ else fs.chmodSync(sessionDir, 0o700);
312
+ } catch { /* best-effort */ }
304
313
  const { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, Browsers } = loadBaileys();
305
314
 
306
315
  const { state, saveCreds } = await useMultiFileAuthState(sessionDir);
@@ -543,6 +552,20 @@ async function startDiscordProvider(config) {
543
552
  }
544
553
  }
545
554
 
555
+ // v5.43 GÜVENLİK: Slack/Signal/IRC/Mattermost'ta gönderen doğrulaması YOKTU + tüm
556
+ // kanallar paylaşımlı 'universal-provider' hafızasını system prompt'a ekliyordu →
557
+ // yetkisiz/yabancı gönderene kişisel hafıza sızabiliyordu. channelGate: (1) allow-list
558
+ // kuruluysa yetkisiz göndereni ENGELLE; (2) allow-list kurulu DEĞİLSE yanıt ver ama
559
+ // kişisel hafızayı ENJEKTE ETME (trusted=false) — böylece anonim kanaldan hafıza sızmaz.
560
+ function channelGate(config, channel, senderId) {
561
+ const allow = config[`${channel}AllowedChats`] || config[`${channel}AllowedNumbers`] || config[`${channel}AllowedUsers`] || [];
562
+ if (!Array.isArray(allow) || allow.length === 0) {
563
+ return { allowed: true, trusted: false };
564
+ }
565
+ const ok = allow.map(String).includes(String(senderId));
566
+ return { allowed: ok, trusted: ok };
567
+ }
568
+
546
569
  async function startSlackProvider(config) {
547
570
  try {
548
571
  const { WebClient } = require('@slack/web-api');
@@ -929,6 +952,10 @@ async function processSignalEnvelope(envelope, config) {
929
952
  const dmPolicy = config.signalDmPolicy || 'pairing';
930
953
  if (dmPolicy === 'disabled') return;
931
954
 
955
+ // v5.43 GÜVENLİK: gönderen doğrulaması + hafıza sızıntısı önleme
956
+ const gate = channelGate(config, 'signal', sender);
957
+ if (!gate.allowed) { log('signal', `Blocked (allow-list): ${sender}`, 'yellow'); return; }
958
+
932
959
  // Get AI response
933
960
  try {
934
961
  const { sendMessage } = require('../utils/api');
@@ -938,7 +965,7 @@ async function processSignalEnvelope(envelope, config) {
938
965
 
939
966
  const conversationId = `signal_${sender}`;
940
967
  const botId = 'universal-provider';
941
- const memoryPrompt = getMemoryPrompt(botId);
968
+ const memoryPrompt = gate.trusted ? getMemoryPrompt(botId) : '';
942
969
 
943
970
  let systemPrompt = `You are a helpful Signal assistant. Keep responses concise.`;
944
971
  if (memoryPrompt) {
@@ -1184,6 +1211,9 @@ function parseIrcLine(line) {
1184
1211
  }
1185
1212
 
1186
1213
  async function processIrcMessage(config, sender, target, text, socket) {
1214
+ // v5.43 GÜVENLİK: gönderen doğrulaması + hafıza sızıntısı önleme
1215
+ const gate = channelGate(config, 'irc', sender);
1216
+ if (!gate.allowed) { log('irc', `Blocked (allow-list): ${sender}`, 'yellow'); return; }
1187
1217
  try {
1188
1218
  const { sendMessage } = require('../utils/api');
1189
1219
  const { getMemoryPrompt } = require('../utils/memory');
@@ -1192,7 +1222,7 @@ async function processIrcMessage(config, sender, target, text, socket) {
1192
1222
 
1193
1223
  const conversationId = `irc_${sender}_${target}`;
1194
1224
  const botId = 'universal-provider';
1195
- const memoryPrompt = getMemoryPrompt(botId);
1225
+ const memoryPrompt = gate.trusted ? getMemoryPrompt(botId) : '';
1196
1226
 
1197
1227
  let systemPrompt = `You are a helpful IRC assistant. Keep responses concise. Use IRC-friendly formatting.`;
1198
1228
  if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
@@ -1370,6 +1400,10 @@ async function handleMattermostPost(data, broadcast, config) {
1370
1400
  // Skip system messages
1371
1401
  if (post.props?.from_webhook) return;
1372
1402
 
1403
+ // v5.43 GÜVENLİK: gönderen doğrulaması + hafıza sızıntısı önleme
1404
+ const gate = channelGate(config, 'mattermost', senderId);
1405
+ if (!gate.allowed) { log('mattermost', `Blocked (allow-list): ${senderId}`, 'yellow'); return; }
1406
+
1373
1407
  // Get AI response
1374
1408
  const { sendMessage } = require('../utils/api');
1375
1409
  const { getMemoryPrompt } = require('../utils/memory');
@@ -1378,7 +1412,7 @@ async function handleMattermostPost(data, broadcast, config) {
1378
1412
 
1379
1413
  const conversationId = `mattermost_${channelId}`;
1380
1414
  const botId = 'universal-provider';
1381
- const memoryPrompt = getMemoryPrompt(botId);
1415
+ const memoryPrompt = gate.trusted ? getMemoryPrompt(botId) : '';
1382
1416
 
1383
1417
  let systemPrompt = `You are a helpful Mattermost assistant. Keep responses concise.`;
1384
1418
  if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
@@ -1578,9 +1612,12 @@ async function processImessageMessage(msg, config) {
1578
1612
  const { getConfig, saveConfig } = require('../utils/config');
1579
1613
  const cfg = getConfig();
1580
1614
 
1615
+ // v5.43 GÜVENLİK: gönderen doğrulaması + hafıza sızıntısı önleme
1616
+ const gate = channelGate(cfg, 'imessage', sender);
1617
+ if (!gate.allowed) { log('imessage', `Blocked (allow-list): ${sender}`, 'yellow'); return; }
1581
1618
  const conversationId = `imessage_${sender}`;
1582
1619
  const botId = 'universal-provider';
1583
- const memoryPrompt = getMemoryPrompt(botId);
1620
+ const memoryPrompt = gate.trusted ? getMemoryPrompt(botId) : '';
1584
1621
 
1585
1622
  let systemPrompt = `You are a helpful iMessage assistant. Keep responses concise.`;
1586
1623
  if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
@@ -1753,9 +1790,12 @@ async function handleSmsWebhook(config, body, req) {
1753
1790
  const { getConfig, saveConfig } = require('../utils/config');
1754
1791
  const cfg = getConfig();
1755
1792
 
1793
+ // v5.43 GÜVENLİK: gönderen doğrulaması + hafıza sızıntısı önleme
1794
+ const gate = channelGate(config, 'sms', from);
1795
+ if (!gate.allowed) return { status: 200, body: { ok: true, blocked: true } };
1756
1796
  const conversationId = `sms_${from}`;
1757
1797
  const botId = 'universal-provider';
1758
- const memoryPrompt = getMemoryPrompt(botId);
1798
+ const memoryPrompt = gate.trusted ? getMemoryPrompt(botId) : '';
1759
1799
 
1760
1800
  let systemPrompt = `You are a helpful SMS assistant. Keep responses concise (SMS format).`;
1761
1801
  if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
@@ -2252,3 +2292,5 @@ if (require.main === module || process.argv.includes('--gateway-worker')) {
2252
2292
  }
2253
2293
 
2254
2294
  module.exports = gatewayServer;
2295
+ // v5.43: test için — kanal gönderen doğrulaması + hafıza izolasyonu (Madde 7)
2296
+ module.exports.channelGate = channelGate;
@@ -40,7 +40,7 @@ const SCHEDULE_PRESETS = {
40
40
  "every sunday": "0 9 * * 0",
41
41
  };
42
42
 
43
- async function createCron({ name, schedule, command, description = "" }) {
43
+ async function createCron({ name, schedule, command, description = "", allowSystemCrontab = false, agentId = "default" }) {
44
44
  if (!name) return { success: false, error: "name gerekli" };
45
45
  if (!schedule) return { success: false, error: "schedule gerekli (cron expression veya preset)" };
46
46
  if (!command) return { success: false, error: "command gerekli (calistirilacak komut)" };
@@ -53,6 +53,16 @@ async function createCron({ name, schedule, command, description = "" }) {
53
53
  return { success: false, error: `Gecersiz cron expression: ${resolvedSchedule}` };
54
54
  }
55
55
 
56
+ // v5.43 GÜVENLİK: command hiç kontrol edilmeden GERÇEK sistem crontab'ina yaziliyordu
57
+ // (persistence — oturum kapansa bile suresiz calisir). Artik: (1) tehlikeli komut hic
58
+ // kaydedilmez; (2) sistem crontab'ina yazma VARSAYILAN OLARAK KAPALI (schema'da param
59
+ // yok → ajan tetikleyemez) ve ancak checkCommand onayindan gecerse yapilir; ajanin
60
+ // olusturdugu cron sadece uygulama-ici crons.json'a yazilir (natureco daemon calistirir).
61
+ const { isDangerousCommand, checkCommand } = require("../utils/approvals");
62
+ if (isDangerousCommand(command)) {
63
+ return { success: false, error: "Tehlikeli komut cron olarak eklenemez (guvenlik). Komutu gozden gecirin." };
64
+ }
65
+
56
66
  const crons = loadCrons();
57
67
  const newCron = {
58
68
  id: genId(),
@@ -64,54 +74,46 @@ async function createCron({ name, schedule, command, description = "" }) {
64
74
  createdAt: new Date().toISOString(),
65
75
  };
66
76
  crons.push(newCron);
67
- saveCrons(crons);
77
+ saveCrons(crons); // uygulama-ici kayit (guvenli, daemon kontrollu calistirir)
68
78
 
69
- // İstege bagli: gercek crontab'a da ekle (sistem cron)
70
- // Bu tehlikeli olabilir, sadece bilgi veriyoruz
71
- let systemCrontabInstalled = false;
72
- try {
73
- const crontabLine = `${resolvedSchedule} ${command} # natureco:${name}`;
74
- const current = execSync("crontab -l 2>/dev/null || echo ''", { encoding: "utf8" });
75
- if (!current.includes(`# natureco:${name}`)) {
76
- // Kullaniciya soru sormadan eklemiyoruz - sadece komutu gosteriyoruz
77
- systemCrontabInstalled = false;
78
- }
79
- } catch {}
80
-
81
- // v5.2.0: Gercek macOS crontab'a ekle (kullanici crontab, sudo gerekmez)
79
+ // Sistem crontab'ina yazma: SADECE acik izin (allowSystemCrontab) + guvenlik onayi ile.
82
80
  const marker = `# natureco:${name}`;
83
81
  let crontabUpdated = false;
84
82
  let crontabError = null;
85
83
 
86
- try {
87
- const { execSync } = require("child_process");
88
- // Mevcut crontab'i oku
89
- let existing = "";
90
- try { existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" }); }
91
- catch { existing = ""; }
92
-
93
- // Ayni isimde var mi kontrol
94
- if (existing.includes(marker)) {
95
- crontabUpdated = false;
84
+ if (allowSystemCrontab) {
85
+ const approval = await checkCommand(command, { agentId });
86
+ if (!approval.allowed) {
87
+ crontabError = `sistem crontab'a yazma reddedildi (${approval.reason})`;
96
88
  } else {
97
- const newLine = `${resolvedSchedule} ${command} ${marker}`;
98
- const updated = existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + newLine + "\n";
99
- // Yeni crontab yukle (heredoc yerine - ile stdin)
100
- execSync("crontab -", { input: updated, encoding: "utf8" });
101
- crontabUpdated = true;
89
+ try {
90
+ let existing = "";
91
+ try { existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" }); }
92
+ catch { existing = ""; }
93
+ if (existing.includes(marker)) {
94
+ crontabUpdated = false;
95
+ } else {
96
+ const safeCmd = approval.editedCommand || command;
97
+ const newLine = `${resolvedSchedule} ${safeCmd} ${marker}`;
98
+ const updated = existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + newLine + "\n";
99
+ execSync("crontab -", { input: updated, encoding: "utf8" });
100
+ crontabUpdated = true;
101
+ }
102
+ } catch (e) {
103
+ crontabError = e.message;
104
+ }
102
105
  }
103
- } catch (e) {
104
- crontabError = e.message;
105
106
  }
106
107
 
107
108
  return {
108
109
  success: true,
109
110
  cron: newCron,
110
111
  message: crontabUpdated
111
- ? `Cron olusturuldu VE macOS crontab'a eklendi: ${name} (${resolvedSchedule})`
112
- : `Cron olusturuldu: ${name} (${resolvedSchedule})${crontabError ? ` - crontab eklenemedi: ${crontabError}` : ""}`,
112
+ ? `Cron olusturuldu VE sistem crontab'a eklendi: ${name} (${resolvedSchedule})`
113
+ : `Cron olusturuldu (uygulama-ici): ${name} (${resolvedSchedule}). Calismasi icin: natureco daemon start.${crontabError ? ` [sistem crontab: ${crontabError}]` : ""}`,
113
114
  crontabUpdated,
114
115
  crontabError,
116
+ systemCrontab: allowSystemCrontab,
115
117
  schedule: resolvedSchedule,
116
118
  };
117
119
  }
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const { expandPath } = require('../utils/paths');
3
3
  const path = require('path');
4
- const { execSync } = require('child_process');
4
+ const { execFileSync } = require('child_process');
5
5
 
6
6
  module.exports = {
7
7
  name: 'document_extract',
@@ -36,7 +36,9 @@ module.exports = {
36
36
  text = fs.readFileSync(filePath, 'utf-8');
37
37
  } else if (ext === '.pdf') {
38
38
  try {
39
- text = execSync(`pdftotext "${filePath}" -`, { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'ignore'] });
39
+ // v5.43 GÜVENLİK: execSync string-interp yerine execFileSync (shell yok)
40
+ // filePath tırnak/backtick/`;` içerse bile komut enjeksiyonu imkansız.
41
+ text = execFileSync('pdftotext', [filePath, '-'], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'ignore'] });
40
42
  } catch {
41
43
  // Fallback: read raw PDF extract
42
44
  const raw = fs.readFileSync(filePath, 'utf-8');
@@ -1,17 +1,27 @@
1
1
  const { getConfig, saveConfig } = require('../utils/config');
2
- const { execSync } = require('child_process');
2
+ const { execFileSync } = require('child_process');
3
3
 
4
4
  function checkAdb() {
5
5
  try {
6
- execSync('adb version', { stdio: 'pipe', encoding: 'utf8' });
6
+ execFileSync('adb', ['version'], { stdio: 'pipe', encoding: 'utf8' });
7
7
  return true;
8
8
  } catch {
9
9
  return false;
10
10
  }
11
11
  }
12
12
 
13
+ // v5.43 GÜVENLİK: `execSync('adb '+args)` shell enjeksiyonuna açıktı. execFileSync +
14
+ // tırnak-farkındalıklı tokenize → shell yok, metakarakter işlem görmez.
15
+ function _tok(s) {
16
+ const out = [];
17
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
18
+ let m;
19
+ while ((m = re.exec(String(s || ''))) !== null) out.push(m[1] ?? m[2] ?? m[3]);
20
+ return out;
21
+ }
22
+
13
23
  function adbCommand(args) {
14
- return execSync(`adb ${args}`, { stdio: 'pipe', encoding: 'utf8', maxBuffer: 1024 * 1024 }).trim();
24
+ return execFileSync('adb', _tok(args), { stdio: 'pipe', encoding: 'utf8', maxBuffer: 1024 * 1024 }).trim();
15
25
  }
16
26
 
17
27
  module.exports = {
@@ -7,6 +7,7 @@
7
7
 
8
8
  const { spawn } = require('child_process');
9
9
  const os = require('os');
10
+ const { isSafeCommand, checkCommand, isDangerousCommand } = require('../utils/approvals');
10
11
 
11
12
  async function runShell({ command, cwd = null, timeoutMs = 10000 }) {
12
13
  if (!command) return { success: false, error: 'command gerekli' };
@@ -50,6 +51,24 @@ module.exports = {
50
51
  required: ['command'],
51
52
  },
52
53
  async execute(params) {
54
+ // v5.43 GÜVENLİK: shell_command da (bash.js gibi) onay/güvenlik akışından geçer.
55
+ // Eski hali doğrudan spawn('bash',...) yapıp checkCommand/isDangerousCommand'ı
56
+ // ATLIYORDU → model/prompt-injection bash yerine bunu çağırıp onaysız sınırsız
57
+ // shell erişimi kazanabiliyordu ("Dangerous Command Approval" tamamen bypass).
58
+ const command = params && params.command;
59
+ if (!command || !command.trim()) return { success: false, error: 'command gerekli' };
60
+ if (!isSafeCommand(command)) {
61
+ const approval = await checkCommand(command, { agentId: (params && params.agentId) || 'default' });
62
+ if (!approval.allowed) {
63
+ return { success: false, error: `Komut güvenlik politikasıyla reddedildi (${approval.reason}). "natureco security" ile politikayı görüntüle.` };
64
+ }
65
+ if (isDangerousCommand(command)) {
66
+ return { success: false, error: 'Tehlikeli komut engellendi. Gözden geçirip manuel çalıştırın.' };
67
+ }
68
+ if (approval.editedCommand && approval.editedCommand !== command) {
69
+ return await runShell({ ...params, command: approval.editedCommand });
70
+ }
71
+ }
53
72
  return await runShell(params);
54
73
  },
55
74
  };
@@ -86,12 +86,18 @@ module.exports = {
86
86
  required: ["message"],
87
87
  },
88
88
  async execute(params) {
89
+ // v5.43 GÜVENLİK: eskiden tespit edilen skill'lerin HAM içeriği doğrulama olmadan
90
+ // system prompt'a enjekte ediliyordu (skillContext) → prompt injection yüzeyi.
91
+ // Artık sadece İSİMLERİ tespit edip döndürür; ajan gerçekten gerekiyorsa
92
+ // skill_view(name) ile KONTROLLÜ yükler (skill-index/skill_view zaten var).
89
93
  const loaded = autoLoad(params.message);
90
94
  return {
91
95
  success: true,
92
96
  message: params.message,
93
97
  detectedSkills: loaded.map(s => s.name),
94
- skillContext: loaded.map(s => s.content).join("\n\n"),
98
+ hint: loaded.length
99
+ ? `İlgili olabilecek skill(ler): ${loaded.map(s => s.name).join(', ')}. Gerekliyse skill_view(name) ile yükle.`
100
+ : 'İlgili skill bulunamadı.',
95
101
  };
96
102
  },
97
103
  };
@@ -36,6 +36,34 @@ const KNOWN_REPOS = [
36
36
  { owner: 'obra', repo: 'superpowers', topic: 'workflow' },
37
37
  ];
38
38
 
39
+ // v5.43 GÜVENLİK: source doğrudan modelden gelip HERHANGİ bir repo indirilebiliyordu
40
+ // (KNOWN_REPOS hiç kullanılmıyordu). İndirilen SKILL.md içeriği skills_autoload ile
41
+ // system prompt'a enjekte edildiğinden → prompt injection → (shell_command bypass'ıyla)
42
+ // RCE zinciri. Artık yalnızca KNOWN_REPOS + kullanıcı onaylı allowlist indirilebilir.
43
+ const SKILLS_ALLOWLIST_FILE = path.join(os.homedir(), '.natureco', 'skills-allowlist.json');
44
+
45
+ function _userAllowlistedRepos() {
46
+ try {
47
+ if (!fs.existsSync(SKILLS_ALLOWLIST_FILE)) return [];
48
+ const arr = JSON.parse(fs.readFileSync(SKILLS_ALLOWLIST_FILE, 'utf8'));
49
+ return Array.isArray(arr) ? arr : [];
50
+ } catch { return []; }
51
+ }
52
+
53
+ function isKnownRepo(owner, repo) {
54
+ const o = (owner || '').toLowerCase(), r = (repo || '').toLowerCase();
55
+ if (!o || !r) return false;
56
+ if (KNOWN_REPOS.some(k => k.owner.toLowerCase() === o && k.repo.toLowerCase() === r)) return true;
57
+ // Kullanıcının açıkça onayladığı ek repolar ("owner/repo" formatında)
58
+ return _userAllowlistedRepos().some(e => String(e).toLowerCase() === `${o}/${r}`);
59
+ }
60
+
61
+ // path-traversal savunması: hedef, base dizininin İÇİNDE mi?
62
+ function _isInside(base, target) {
63
+ const rel = path.relative(base, target);
64
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
65
+ }
66
+
39
67
  function httpsGet(url) {
40
68
  return new Promise((resolve, reject) => {
41
69
  https.get(url, { timeout: 15000 }, (res) => {
@@ -115,7 +143,16 @@ async function fetchGithubRepo(owner, repo) {
115
143
  }
116
144
 
117
145
  async function downloadSkill(skill, targetDir) {
118
- const skillDir = path.join(targetDir, skill.name);
146
+ // v5.43 GÜVENLİK: skill.name repo'dan gelir; "../../.bashrc" gibi traversal olabilir.
147
+ // Sadece dosya adı bileşenini kullan.
148
+ const safeName = path.basename(String(skill.name || '').trim());
149
+ if (!safeName || safeName === '.' || safeName === '..' || safeName.includes('/') || safeName.includes('\\')) {
150
+ return { success: false, error: 'Geçersiz skill adı' };
151
+ }
152
+ const skillDir = path.join(targetDir, safeName);
153
+ if (!_isInside(targetDir, skillDir)) {
154
+ return { success: false, error: 'Güvenlik: skill dizini hedef dışına çıkıyor' };
155
+ }
119
156
  if (fs.existsSync(skillDir)) {
120
157
  return { success: false, error: 'Already exists', path: skillDir };
121
158
  }
@@ -139,14 +176,16 @@ async function downloadSkill(skill, targetDir) {
139
176
  // Download additional files
140
177
  for (const af of (skill.additionalFiles || [])) {
141
178
  try {
179
+ // v5.43 GÜVENLİK: af.path traversal ("../../../../etc/x") → skillDir DIŞINA yazma.
180
+ const dest = path.resolve(skillDir, af.path);
181
+ if (!_isInside(skillDir, dest)) continue; // traversal → atla
142
182
  const data = await httpsGet(af.url);
143
- const afDir = path.dirname(path.join(skillDir, af.path));
144
- fs.mkdirSync(afDir, { recursive: true });
145
- fs.writeFileSync(path.join(skillDir, af.path), data, 'utf8');
183
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
184
+ fs.writeFileSync(dest, data, 'utf8');
146
185
  } catch {}
147
186
  }
148
187
 
149
- return { success: true, path: skillDir, name: skill.name };
188
+ return { success: true, path: skillDir, name: safeName };
150
189
  }
151
190
 
152
191
  async function execute(params) {
@@ -161,6 +200,9 @@ async function execute(params) {
161
200
  if (action === 'list_skills') {
162
201
  const [owner, repo] = (source || '').split('/');
163
202
  if (!owner || !repo) return JSON.stringify({ success: false, error: 'source must be owner/repo (e.g. anthropics/skills)' });
203
+ if (!isKnownRepo(owner, repo)) {
204
+ return JSON.stringify({ success: false, error: `Güvenlik: "${owner}/${repo}" bilinen/onaylı kaynaklar arasında değil. "natureco security skills-allowlist add ${owner}/${repo}" ile ekleyin veya "list_sources" ile bilinen kaynakları görün.` });
205
+ }
164
206
  const skills = await fetchGithubRepo(owner, repo);
165
207
  return JSON.stringify({ success: true, source, count: skills.length, skills: skills.map(s => ({ name: s.name, description: s.description })) });
166
208
  }
@@ -168,6 +210,9 @@ async function execute(params) {
168
210
  if (action === 'download') {
169
211
  const [owner, repo] = (source || '').split('/');
170
212
  if (!owner || !repo) return JSON.stringify({ success: false, error: 'source must be owner/repo' });
213
+ if (!isKnownRepo(owner, repo)) {
214
+ return JSON.stringify({ success: false, error: `Güvenlik: "${owner}/${repo}" bilinen/onaylı kaynaklar arasında değil. Model kendi kendine keyfi repo indiremez; kullanıcı "natureco security skills-allowlist add ${owner}/${repo}" ile açıkça onaylamalı.` });
215
+ }
171
216
  const skills = await fetchGithubRepo(owner, repo);
172
217
  const targetSkill = name ? skills.find(s => s.name === name) : null;
173
218
  const toDownload = targetSkill ? [targetSkill] : skills;
@@ -214,4 +259,4 @@ const parameters = {
214
259
  required: ['action'],
215
260
  };
216
261
 
217
- module.exports = { name, description, parameters, execute };
262
+ module.exports = { name, description, parameters, execute, isKnownRepo, downloadSkill, _isInside, KNOWN_REPOS };