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.
- package/CHANGELOG.md +22 -0
- package/README.md +578 -609
- package/SECURITY_AUDIT_SUMMARY.md +40 -0
- package/package.json +2 -1
- package/src/commands/admin-rpc.js +84 -10
- package/src/commands/gateway-server.js +47 -5
- package/src/tools/cron_create.js +36 -34
- package/src/tools/document_extract.js +4 -2
- package/src/tools/phone_control_enhanced.js +13 -3
- package/src/tools/shell_command.js +19 -0
- package/src/tools/skills_autoload.js +7 -1
- package/src/tools/skills_download.js +51 -6
- package/src/tools/social_open.js +2 -2
- package/src/tools/youtube_ac.js +2 -2
- package/src/utils/approvals.js +15 -4
- package/src/utils/config.js +17 -4
- package/src/utils/tool-runner.js +3 -1
|
@@ -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.
|
|
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')
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 ağ 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;
|
package/src/tools/cron_create.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
87
|
-
const {
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
112
|
-
: `Cron olusturuldu: ${name} (${resolvedSchedule})
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
2
|
+
const { execFileSync } = require('child_process');
|
|
3
3
|
|
|
4
4
|
function checkAdb() {
|
|
5
5
|
try {
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
fs.
|
|
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:
|
|
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 };
|