# GHSA-cfcj-hqpf-hccf: The `evolver fetch` subcommand in `index.js` writes Hub-supplied `bundled_files[]` into a directory derived from a Hub-supplied `skill_id`.

> A malicious Hub can overwrite project files via path traversal in evolver fetch, leading to remote code execution on the next invocation.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Not needed
- **Attackable:** Over the internet
- **Victim action needed:** A user has to click or open something
- **Fix available:** Yes
- **Published:** 5 May 2026

## What is GHSA-cfcj-hqpf-hccf?

GHSA-cfcj-hqpf-hccf is a path traversal vulnerability in @evomap/evolver, published on 5 May 2026. An attacker can exploit it over the internet without logging in, and only if a user opens a file or follows a link.

### Official description

## 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`.

## Which versions are affected by GHSA-cfcj-hqpf-hccf?

2 open source packages ship an affected version, including @evomap/evolver.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `@evomap/evolver` | npm | All versions | 1.70.0-beta.5 |
| `@evomap/evolver` | npm | All versions | 1.70.0-beta.5 |

## How do I fix GHSA-cfcj-hqpf-hccf?

Upgrade @evomap/evolver to 1.70.0-beta.5 or later.

## Is GHSA-cfcj-hqpf-hccf being exploited?

There is no known exploitation of GHSA-cfcj-hqpf-hccf in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-cfcj-hqpf-hccf and fix it if it is affected.

What it is: GHSA-cfcj-hqpf-hccf is a path traversal vulnerability in @evomap/evolver, published on 5 May 2026. An attacker can exploit it over the internet without logging in, and only if a user opens a file or follows a link.
Exploitation: There is no known exploitation of GHSA-cfcj-hqpf-hccf in the wild and no public exploit code.

Affected packages and fixed versions:
- @evomap/evolver (npm): affected all versions; fixed in 1.70.0-beta.5
- @evomap/evolver (npm): affected all versions; fixed in 1.70.0-beta.5

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-cfcj-hqpf-hccf/ (Markdown: https://insights.sbomify.com/GHSA-cfcj-hqpf-hccf/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-cfcj-hqpf-hccf

---
From sbomify Insights: https://insights.sbomify.com/GHSA-cfcj-hqpf-hccf/
Summaries are generated and may contain mistakes; the official description above is the record.
