@0dai-dev/cli 3.3.1 → 3.3.2
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/bin/0dai.js +53 -12
- package/package.json +1 -1
package/bin/0dai.js
CHANGED
|
@@ -8,7 +8,26 @@ const path = require("path");
|
|
|
8
8
|
const os = require("os");
|
|
9
9
|
|
|
10
10
|
const VERSION = require("../package.json").version;
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
// CRITICAL: validate API_URL to prevent credential exfiltration to attacker hosts
|
|
13
|
+
function _validateApiUrl(url) {
|
|
14
|
+
const DEFAULT = "https://api.0dai.dev";
|
|
15
|
+
if (!url) return DEFAULT;
|
|
16
|
+
try {
|
|
17
|
+
const u = new URL(url);
|
|
18
|
+
// Only allow https (or http for localhost during dev)
|
|
19
|
+
if (u.protocol === "https:") return url;
|
|
20
|
+
if (u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1")) {
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
console.error(`[0dai] warning: ODAI_API_URL must use https (or http://localhost). Got: ${u.protocol}//${u.hostname}. Ignoring.`);
|
|
24
|
+
return DEFAULT;
|
|
25
|
+
} catch {
|
|
26
|
+
console.error(`[0dai] warning: ODAI_API_URL is not a valid URL, ignoring`);
|
|
27
|
+
return DEFAULT;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const API_URL = _validateApiUrl(process.env.ODAI_API_URL);
|
|
12
31
|
const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
|
|
13
32
|
const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
|
|
14
33
|
const D = process.stdout.isTTY ? "\x1b[2m" : ""; // dim
|
|
@@ -321,17 +340,20 @@ function mergeSettingsJson(existing, incoming) {
|
|
|
321
340
|
} catch { return incoming; }
|
|
322
341
|
}
|
|
323
342
|
|
|
324
|
-
function mergeAgentsMd(existing, incoming) {
|
|
325
|
-
// If user marked as unmanaged, don't touch
|
|
326
|
-
if (existing.includes("managed: false")) return existing;
|
|
327
|
-
// If managed, update with new content but preserve user additions after managed block
|
|
328
|
-
return incoming;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
343
|
function writeFiles(target, files) {
|
|
332
|
-
let created = 0, updated = 0, unchanged = 0, merged = 0;
|
|
344
|
+
let created = 0, updated = 0, unchanged = 0, merged = 0, skipped = 0;
|
|
345
|
+
const targetResolved = path.resolve(target);
|
|
333
346
|
for (const [rel, content] of Object.entries(files)) {
|
|
334
|
-
|
|
347
|
+
// HIGH: path traversal protection — reject absolute paths and `..` escapes
|
|
348
|
+
if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
|
|
349
|
+
skipped++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const p = path.resolve(targetResolved, rel);
|
|
353
|
+
if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) {
|
|
354
|
+
skipped++;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
335
357
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
336
358
|
|
|
337
359
|
let finalContent = content;
|
|
@@ -365,6 +387,7 @@ function writeFiles(target, files) {
|
|
|
365
387
|
}
|
|
366
388
|
const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
|
|
367
389
|
if (merged) parts.push(`${merged} merged`);
|
|
390
|
+
if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
|
|
368
391
|
log(parts.join(", "));
|
|
369
392
|
return created + updated;
|
|
370
393
|
}
|
|
@@ -1446,9 +1469,10 @@ async function cmdAuthLogin() {
|
|
|
1446
1469
|
const url = `${API_URL}/v1/auth/${method}?cli=true`;
|
|
1447
1470
|
p.log.info(`Opening browser: ${url}`);
|
|
1448
1471
|
try {
|
|
1449
|
-
const {
|
|
1472
|
+
const { execFileSync } = require("child_process");
|
|
1450
1473
|
const cmd = os.platform() === "darwin" ? "open" : os.platform() === "win32" ? "start" : "xdg-open";
|
|
1451
|
-
|
|
1474
|
+
// MED: use execFileSync to avoid shell injection via URL metacharacters
|
|
1475
|
+
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
1452
1476
|
} catch {
|
|
1453
1477
|
p.log.warn(`Could not open browser. Visit manually:\n ${url}`);
|
|
1454
1478
|
}
|
|
@@ -1791,6 +1815,23 @@ function cmdSwarm(target, sub, args) {
|
|
|
1791
1815
|
const event = args.find((_, i) => args[i-1] === "--event") || "all";
|
|
1792
1816
|
const secret = args.find((_, i) => args[i-1] === "--secret") || "";
|
|
1793
1817
|
if (!url || !url.startsWith("http")) { log("Usage: 0dai swarm webhook add <url> [--event task_done|task_failed|all] [--secret TOKEN]"); return; }
|
|
1818
|
+
// MED: SSRF protection — block internal/metadata endpoints
|
|
1819
|
+
try {
|
|
1820
|
+
const u = new URL(url);
|
|
1821
|
+
const host = u.hostname;
|
|
1822
|
+
const BLOCKED = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|::1$|fc00:|fe80:|localhost$|0\.0\.0\.0$)/i;
|
|
1823
|
+
if (BLOCKED.test(host) || host === "metadata.google.internal") {
|
|
1824
|
+
log(`rejected: ${host} is a private/internal address (SSRF protection)`);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
1828
|
+
log(`rejected: only http/https allowed, got ${u.protocol}`);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
} catch {
|
|
1832
|
+
log(`invalid URL: ${url}`);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1794
1835
|
const hooks = loadHooks();
|
|
1795
1836
|
if (hooks.find(h => h.url === url)) { log(`already registered: ${url}`); return; }
|
|
1796
1837
|
hooks.push({ url, event, secret: secret || undefined, added_at: new Date().toISOString() });
|