GitHub Advisoryadvisory · npm
GHSA-48x2-6pr9-2jjf Network-AI: EnvironmentManager.restore() backup ID path traversal copies arbitrary directories into environment data
MEDIUM
Official page ↗
- Ecosystem
- npm
- Packages
- network-ai
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
- Weaknesses
- CWE-22, CWE-23
- Reviewed
- 2026-06-19
Affected packages
• npm network-ai: < 5.12.2 → fixed in 5.12.2
Description
Summary
`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//.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. Confirmed in Network-AI 5.12.1.
Details
`restore()` builds `backupPath` directly from caller-controlled `backupId`:
```ts
restore(env: EnvName, backupId: string): RestoreResult {
const envDir = this.getDataDir(env);
const backupsDir = join(envDir, '.backups');
const backupPath = join(backupsDir, backupId);
if (!existsSync(backupPath)) {
throw new Error(`Backup '${backupId}' not found for environment '${env}'`);
}
this.backup(env);
const files = this._collectBackupFiles(backupPath);
let restored = 0;
for (const rel of files) {
if (rel === '_manifest.json') continue;
const src = join(backupPath, rel);
const dst = join(envDir, rel);
try {
mkdirSync(join(envDir, rel.includes('/') ? rel.substring(0, rel.lastIndexOf('/')) : '.'), { recursive: true });
copyFileSync(src, dst);
restored++;
} catch { /* skip */ }
}
return { backupId, env, filesRestored: restored };
}
```
There is no resolved containment check that ensures `backupPath` remains under `backupsDir`.
Default CLI reachability exists through `network-ai env backup restore --env --backup `.
Affected source evidence:
• `lib/env-manager.ts:474-499` — vulnerable restore path construction and copy.
• `bin/cli.ts:441-458` — default CLI exposes restore with caller-controlled `--backup`.
PoC
This PoC uses only temporary directories and restores `trust_levels.json` from an external directory into `data/dev`:
```bash
TMP=$(mktemp -d)
TMPBASE="$TMP" node -r ts-node/register/transpile-only - `, 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.
---
Resolution (maintainer)
Fixed in v5.12.2 (commit `a59c13a`). Install: `npm install [email protected]` — published to npm with provenance.
`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.
All 3,269 tests pass against the patched build. Thanks to @sondt99 for the responsible disclosure.