GHSA-cfcj-hqpf-hccf: @evomap/evolver Remote Code Execution Vulnerability
HIGHGHSA-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
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).
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.
npm install @evomap/evolver@1.70.0-beta.5
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
GHSA-cfcj-hqpf-hccf Affected @evomap/evolver Versions
1 component(s)| 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?
Which @evomap/evolver versions are affected?
What version fixes GHSA-cfcj-hqpf-hccf?
Is @evomap/evolver affected?
What is the CVSS score of GHSA-cfcj-hqpf-hccf?
What CWE is associated with GHSA-cfcj-hqpf-hccf?
How can I check npm dependencies for GHSA-cfcj-hqpf-hccf?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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
}
}