GHSA-cfcj-hqpf-hccf: @evomap/evolver Remote Code Execution Vulnerability

HIGH
CVSS 8.8
CWE-73
Published
Advisory Summary

GHSA-cfcj-hqpf-hccf is a HIGH (CVSS 8.8) remote code execution vulnerability (CWE-73: External Control of File Name or Path) affecting @evomap/evolver libraries in the npm ecosystem. The issue involves path traversal in `evolver fetch` default-branch `safeid` allows hub-controlled overwrite of project files (rce). Remediation is available by upgrading affected packages to version 1.70.0-beta.5.

GHSA-cfcj-hqpf-hccf at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-cfcj-hqpf-hccf
Severity HIGH
CVSS v3.1 Base Score 8.8
CWE Classification CWE-73: External Control of File Name or Path
Affected Product @evomap/evolver
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 8.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix GHSA-cfcj-hqpf-hccf

To remediate this vulnerability, upgrade all affected @evomap/evolver dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@evomap/evolver fixed: 1.70.0-beta.5
npm install @evomap/evolver@1.70.0-beta.5
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @evomap/evolver

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-cfcj-hqpf-hccf, inspect your project dependencies using npm:

npm ls @evomap/evolver

Analyze the terminal output to locate any version of @evomap/evolver in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.70.0-beta.5), your application is affected.

Note: The npm ls @evomap/evolver command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-73: External Control of File Name or Path, where the product allows user input to control or influence paths or file names that are used in filesystem operations. In @evomap/evolver, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary The `evolver fetch` subcommand in `index.js` writes Hub-supplied `bundled_files[]` into a directory derived from a Hub-supplied `skill_id`. When `--out` is not used, the path-sanitizing regex permits `.` characters, allowing a `skill_id` of `..` to escape the `skills/` subdirectory and resolve to the user's current working directory. Combined with the file-extension allow-list (which includes `.js`/`.json`/`.sh`/`.py`/`.md`), this lets a malicious Hub overwrite the victim's `index.js`, `package.json`, or other files in cwd, achieving remote code execution on the next invocation of the evolver. ## Details The vulnerable code is in the `fetch` command handler: ```js // index.js:847-873 const data = await resp.json(); const outFlag = args.find(a => typeof a === 'string' && a.startsWith('--out=')); const safeId = String(data.skill_id || skillId).replace(/[^a-zA-Z0-9_\-\.]/g, '_'); let outDir; if (outFlag) { const rawOut = outFlag.slice('--out='.length); // ... const resolvedOut = path.resolve(process.cwd(), rawOut); const cwd = path.resolve(process.cwd()); const rel = path.relative(cwd, resolvedOut); if (rel.startsWith('..') || path.isAbsolute(rel)) { // <-- traversal check exists for --out console.error('[fetch] --out= must resolve to a path inside the current working directory'); process.exit(1); } outDir = resolvedOut; } else { outDir = path.join('.', 'skills', safeId); // <-- NO traversal check } if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); ``` Three problems compose: 1. **The regex allow-list permits `.`** — `[^a-zA-Z0-9_\-\.]` only strips characters *outside* this set, so the literal dot is preserved. A `skill_id` of `..` (verified: `'..'.replace(/[^a-zA-Z0-9_\-\.]/g,'_') === '..'`) survives sanitization. 2. **`path.join` collapses `..` traversal** — `path.join('.', 'skills', '..')` evaluates to `'.'` (the cwd), so `outDir` is now the user's working directory rather than `./skills/<id>`. 3. **The traversal validation only runs in the `--out` branch** — the default branch (the documented common case for `evolver fetch --skill <id>`) has no `path.relative(...).startsWith('..')` check. The bundled-files write loop: ```js // index.js:881-906 const ALLOWED_SKILL_EXTENSIONS = new Set([ '.js', '.mjs', '.cjs', '.ts', '.json', '.md', '.txt', '.sh', '.py', '.yml', '.yaml', ]); // ... for (const file of bundled) { if (!file || !file.name || typeof file.content !== 'string') continue; const safeName = path.basename(file.name); // basename of "index.js" is "index.js" const ext = path.extname(safeName).toLowerCase(); if (!ALLOWED_SKILL_EXTENSIONS.has(ext)) { /* skip */ continue; } if (Buffer.byteLength(file.content, 'utf8') > MAX_SKILL_FILE_BYTES) { /* skip */ continue; } fs.writeFileSync(path.join(outDir, safeName), file.content, 'utf8'); } ``` `path.basename` strips directory components from the *file name*, but a basename of `index.js` is still `index.js`. The extension allow-list contains `.js`, so an attacker can write `./index.js` (the evolver entry point itself), `./package.json`, `./SKILL.md`, etc. There is no signature verification on the Hub response. `buildHubHeaders()` only authenticates the *outgoing* request; the response body is trusted as-is. The Hub stores skills uploaded by network participants, so any participant who can set a stored `skill_id` field to `..` triggers this on every download. ## PoC Reproduces the exact code path from `index.js:849-905`: ```bash cd /tmp && rm -rf evolver-poc-validate && mkdir evolver-poc-validate && \ cp /path/to/EvoMap-evolver-src/index.js evolver-poc-validate/ cd evolver-poc-validate wc -l index.js # 1098 index.js (legitimate) node -e " const fs=require('fs'),path=require('path'); const data={ skill_id:'..', content:'x', bundled_files:[{name:'index.js',content:'#!/usr/bin/env node\nconsole.log(\"PWNED\");'}] }; const safeId=String(data.skill_id||'x').replace(/[^a-zA-Z0-9_\-\.]/g,'_'); const outDir=path.join('.','skills',safeId); console.log('safeId:',JSON.stringify(safeId)); // '..' console.log('outDir:',JSON.stringify(outDir)); // '.' if(!fs.existsSync(outDir))fs.mkdirSync(outDir,{recursive:true}); for(const f of data.bundled_files){ const n=path.basename(f.name); fs.writeFileSync(path.join(outDir,n),f.content); }" wc -l index.js # 1 index.js (clobbered) head -3 index.js # #!/usr/bin/env node # console.log("PWNED"); ``` Verified output: 1098 → 1 line; the legitimate evolver entry point is replaced with attacker-controlled JavaScript. Any subsequent `node index.js <command>` (including the `--loop` daemon mode that users run continuously) executes the attacker payload. End-to-end attack: 1. Attacker uploads a skill to the A2A Hub whose stored `skill_id` is `..` (or operates a malicious Hub / MitMs the connection / supplies a malicious `A2A_HUB_URL`). 2. The malicious response also carries `bundled_files: [{name: 'index.js', content: '<attacker JS>'}]`. 3. Victim runs `node index.js fetch --skill=anything` from the evolver checkout (the documented usage). 4. `./index.js` is overwritten in place. 5. Victim's next `node index.js` invocation — even just `node index.js --help` or the `run --loop` daemon — executes attacker code with the victim's privileges. ## Impact - **Remote code execution** in the victim's environment with the privileges of the evolver process. Because the loop daemon (`node index.js run --loop`) is the documented long-running mode, the malicious code typically gets executed within seconds of the next iteration. - Attacker can also overwrite `package.json` (allowed extension), `SKILL.md`, `.env`-adjacent `.json`/`.yaml`/`.yml` config files, and any whitelisted file already present in the cwd. - Trust boundary violation: `evolver fetch` is presented as a *download* operation; users would not expect it to overwrite the application binary or project files. The `--out` branch was hardened against exactly this; the default branch was missed. - A single malicious skill upload compromises every user that fetches it. ## Recommended Fix Reject `safeId` values that are not single non-traversing path segments before joining, or reuse the same `path.relative` check used in the `--out` branch. Minimal patch around `index.js:849`: ```js const safeId = String(data.skill_id || skillId).replace(/[^a-zA-Z0-9_\-\.]/g, '_'); if ( safeId === '' || safeId === '.' || safeId === '..' || safeId.includes('/') || safeId.includes('\\') || safeId.includes('\0') ) { console.error('[fetch] Hub returned an invalid skill_id: ' + JSON.stringify(safeId)); process.exit(1); } ``` Defense in depth — apply the existing traversal check to the default branch as well: ```js } else { const candidate = path.resolve(process.cwd(), 'skills', safeId); const skillsRoot = path.resolve(process.cwd(), 'skills'); const rel = path.relative(skillsRoot, candidate); if (rel.startsWith('..') || path.isAbsolute(rel)) { console.error('[fetch] Hub returned a skill_id that escapes the skills/ directory'); process.exit(1); } outDir = candidate; } ``` Additionally, consider: - Removing `.` from the regex allow-list (skill IDs typically don't need dots). - Verifying a Hub-supplied signature over the response payload before writing any file to disk. - Disallowing bundled-file `safeName` values that match top-level project files (`index.js`, `package.json`, `package-lock.json`, etc.) regardless of `outDir`.

GHSA-cfcj-hqpf-hccf Affected @evomap/evolver Versions

1 component(s)
Fixed Versions:
1.70.0-beta.5
Product / Package Ecosystem Affected Versions Fixed Version
@evomap/evolver
npm
< 1.70.0-beta.5
1.70.0-beta.5

References

Frequently Asked Questions

What is GHSA-cfcj-hqpf-hccf?
GHSA-cfcj-hqpf-hccf is a security vulnerability in @evomap/evolver involving Path Traversal in `evolver fetch` default-branch `safeId` allows Hub-controlled overwrite of project files (RCE) under CWE-73: External Control of File Name or Path.
Which @evomap/evolver versions are affected?
Affected versions include releases prior to 1.70.0-beta.5 across affected packages: @evomap/evolver.
What version fixes GHSA-cfcj-hqpf-hccf?
Upgrading to 1.70.0-beta.5 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @evomap/evolver affected?
Yes, @evomap/evolver is affected in releases prior to 1.70.0-beta.5. Upgrade your dependency to 1.70.0-beta.5 or later.
What is the CVSS score of GHSA-cfcj-hqpf-hccf?
GHSA-cfcj-hqpf-hccf has a CVSS v3.1 base score of 8.8 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H.
What CWE is associated with GHSA-cfcj-hqpf-hccf?
GHSA-cfcj-hqpf-hccf is classified under CWE-73: External Control of File Name or Path (The product allows user input to control or influence paths or file names that are used in filesystem operations.).
How can I check npm dependencies for GHSA-cfcj-hqpf-hccf?
Run the dependency tree command: 'npm ls @evomap/evolver'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-cfcj-hqpf-hccf",
  "modified" : "2026-05-05T21:15:09Z",
  "published" : "2026-05-05T21:15:09Z",
  "aliases" : [ ],
  "summary" : "@evomap/evolver: Path Traversal in `evolver fetch` default-branch `safeId` allows Hub-controlled overwrite of project files (RCE)",
  "details" : "## Summary\n\nThe `evolver fetch` subcommand in `index.js` writes Hub-supplied `bundled_files[]` into a directory derived from a Hub-supplied `skill_id`. When `--out` is not used, the path-sanitizing regex permits `.` characters, allowing a `skill_id` of `..` to escape the `skills/` subdirectory and resolve to the user's current working directory. Combined with the file-extension allow-list (which includes `.js`/`.json`/`.sh`/`.py`/`.md`), this lets a malicious Hub overwrite the victim's `index.js`, `package.json`, or other files in cwd, achieving remote code execution on the next invocation of the evolver.\n\n## Details\n\nThe vulnerable code is in the `fetch` command handler:\n\n```js\n// index.js:847-873\nconst data = await resp.json();\nconst outFlag = args.find(a => typeof a === 'string' && a.startsWith('--out='));\nconst safeId = String(data.skill_id || skillId).replace(/[^a-zA-Z0-9_\\-\\.]/g, '_');\nlet outDir;\nif (outFlag) {\n  const rawOut = outFlag.slice('--out='.length);\n  // ...\n  const resolvedOut = path.resolve(process.cwd(), rawOut);\n  const cwd = path.resolve(process.cwd());\n  const rel = path.relative(cwd, resolvedOut);\n  if (rel.startsWith('..') || path.isAbsolute(rel)) {     // <-- traversal check exists for --out\n    console.error('[fetch] --out= must resolve to a path inside the current working directory');\n    process.exit(1);\n  }\n  outDir = resolvedOut;\n} else {\n  outDir = path.join('.', 'skills', safeId);              // <-- NO traversal check\n}\n\nif (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });\n```\n\nThree problems compose:\n\n1. **The regex allow-list permits `.`** — `[^a-zA-Z0-9_\\-\\.]` only strips characters *outside* this set, so the literal dot is preserved. A `skill_id` of `..` (verified: `'..'.replace(/[^a-zA-Z0-9_\\-\\.]/g,'_') === '..'`) survives sanitization.\n2. **`path.join` collapses `..` traversal** — `path.join('.', 'skills', '..')` evaluates to `'.'` (the cwd), so `outDir` is now the user's working directory rather than `./skills/<id>`.\n3. **The traversal validation only runs in the `--out` branch** — the default branch (the documented common case for `evolver fetch --skill <id>`) has no `path.relative(...).startsWith('..')` check.\n\nThe bundled-files write loop:\n\n```js\n// index.js:881-906\nconst ALLOWED_SKILL_EXTENSIONS = new Set([\n  '.js', '.mjs', '.cjs', '.ts', '.json', '.md', '.txt',\n  '.sh', '.py', '.yml', '.yaml',\n]);\n// ...\nfor (const file of bundled) {\n  if (!file || !file.name || typeof file.content !== 'string') continue;\n  const safeName = path.basename(file.name);                       // basename of \"index.js\" is \"index.js\"\n  const ext = path.extname(safeName).toLowerCase();\n  if (!ALLOWED_SKILL_EXTENSIONS.has(ext)) { /* skip */ continue; }\n  if (Buffer.byteLength(file.content, 'utf8') > MAX_SKILL_FILE_BYTES) { /* skip */ continue; }\n  fs.writeFileSync(path.join(outDir, safeName), file.content, 'utf8');\n}\n```\n\n`path.basename` strips directory components from the *file name*, but a basename of `index.js` is still `index.js`. The extension allow-list contains `.js`, so an attacker can write `./index.js` (the evolver entry point itself), `./package.json`, `./SKILL.md`, etc.\n\nThere is no signature verification on the Hub response. `buildHubHeaders()` only authenticates the *outgoing* request; the response body is trusted as-is. The Hub stores skills uploaded by network participants, so any participant who can set a stored `skill_id` field to `..` triggers this on every download.\n\n## PoC\n\nReproduces the exact code path from `index.js:849-905`:\n\n```bash\ncd /tmp && rm -rf evolver-poc-validate && mkdir evolver-poc-validate && \\\n  cp /path/to/EvoMap-evolver-src/index.js evolver-poc-validate/\ncd evolver-poc-validate\nwc -l index.js                                  # 1098 index.js (legitimate)\n\nnode -e \"\nconst fs=require('fs'),path=require('path');\nconst data={\n  skill_id:'..',\n  content:'x',\n  bundled_files:[{name:'index.js',content:'#!/usr/bin/env node\\nconsole.log(\\\"PWNED\\\");'}]\n};\nconst safeId=String(data.skill_id||'x').replace(/[^a-zA-Z0-9_\\-\\.]/g,'_');\nconst outDir=path.join('.','skills',safeId);\nconsole.log('safeId:',JSON.stringify(safeId));   // '..'\nconsole.log('outDir:',JSON.stringify(outDir));   // '.'\nif(!fs.existsSync(outDir))fs.mkdirSync(outDir,{recursive:true});\nfor(const f of data.bundled_files){\n  const n=path.basename(f.name);\n  fs.writeFileSync(path.join(outDir,n),f.content);\n}\"\n\nwc -l index.js                                  # 1 index.js  (clobbered)\nhead -3 index.js\n# #!/usr/bin/env node\n# console.log(\"PWNED\");\n```\n\nVerified output: 1098 → 1 line; the legitimate evolver entry point is replaced with attacker-controlled JavaScript. Any subsequent `node index.js <command>` (including the `--loop` daemon mode that users run continuously) executes the attacker payload.\n\nEnd-to-end attack:\n1. Attacker uploads a skill to the A2A Hub whose stored `skill_id` is `..` (or operates a malicious Hub / MitMs the connection / supplies a malicious `A2A_HUB_URL`).\n2. The malicious response also carries `bundled_files: [{name: 'index.js', content: '<attacker JS>'}]`.\n3. Victim runs `node index.js fetch --skill=anything` from the evolver checkout (the documented usage).\n4. `./index.js` is overwritten in place.\n5. Victim's next `node index.js` invocation — even just `node index.js --help` or the `run --loop` daemon — executes attacker code with the victim's privileges.\n\n## Impact\n\n- **Remote code execution** in the victim's environment with the privileges of the evolver process. Because the loop daemon (`node index.js run --loop`) is the documented long-running mode, the malicious code typically gets executed within seconds of the next iteration.\n- Attacker can also overwrite `package.json` (allowed extension), `SKILL.md`, `.env`-adjacent `.json`/`.yaml`/`.yml` config files, and any whitelisted file already present in the cwd.\n- Trust boundary violation: `evolver fetch` is presented as a *download* operation; users would not expect it to overwrite the application binary or project files. The `--out` branch was hardened against exactly this; the default branch was missed.\n- A single malicious skill upload compromises every user that fetches it.\n\n## Recommended Fix\n\nReject `safeId` values that are not single non-traversing path segments before joining, or reuse the same `path.relative` check used in the `--out` branch. Minimal patch around `index.js:849`:\n\n```js\nconst safeId = String(data.skill_id || skillId).replace(/[^a-zA-Z0-9_\\-\\.]/g, '_');\nif (\n  safeId === '' ||\n  safeId === '.' ||\n  safeId === '..' ||\n  safeId.includes('/') ||\n  safeId.includes('\\\\') ||\n  safeId.includes('\\0')\n) {\n  console.error('[fetch] Hub returned an invalid skill_id: ' + JSON.stringify(safeId));\n  process.exit(1);\n}\n```\n\nDefense in depth — apply the existing traversal check to the default branch as well:\n\n```js\n} else {\n  const candidate = path.resolve(process.cwd(), 'skills', safeId);\n  const skillsRoot = path.resolve(process.cwd(), 'skills');\n  const rel = path.relative(skillsRoot, candidate);\n  if (rel.startsWith('..') || path.isAbsolute(rel)) {\n    console.error('[fetch] Hub returned a skill_id that escapes the skills/ directory');\n    process.exit(1);\n  }\n  outDir = candidate;\n}\n```\n\nAdditionally, consider:\n- Removing `.` from the regex allow-list (skill IDs typically don't need dots).\n- Verifying a Hub-supplied signature over the response payload before writing any file to disk.\n- Disallowing bundled-file `safeName` values that match top-level project files (`index.js`, `package.json`, `package-lock.json`, etc.) regardless of `outDir`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@evomap/evolver"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.70.0-beta.5"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.70.0-beta.4"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/EvoMap/evolver/security/advisories/GHSA-cfcj-hqpf-hccf"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/EvoMap/evolver"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-73" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-05T21:15:09Z",
    "nvd_published_at" : null
  }
}