GHSA-p498-v437-472g json
humanfs: Recursive copy follows symlinked files and copies data from outside the source tree
published 4mo ago modified 2mo ago
- cvss
- 5.7
- version 4.0, the maintainers
- cisa kev
- Not listed
- not in CISA's catalog
- epss
- —
- no score, EPSS covers CVEs only
- public exploits
- 0
- none known
Summary
### Summary
`@humanfs/node` does not treat symlinks as a separate case during copy operations. A symlink placed inside an attacker-controlled source tree can make `copyAll()` read and copy the contents of any file readable by the process, even when that file is outside the directory being copied.
### Details
The Node implementation exposes symlink state through `list()`, but `copyAll()` ignores it. During recursive copies, every non-directory entry is passed to `copy()`, which delegates to `fs.promises.copyFile()`.
On Node, `copyFile()` dereferences symlinks. As a result, a symlink inside the copied tree is handled as if it were an ordinary file, and the destination receives the contents of the symlink target rather than a copy of the link itself.
That breaks the expected boundary of a directory copy. A caller can point `copyAll()` at a seemingly self-contained directory and still end up copying data from elsewhere on the host filesystem if the source tree contains attacker-supplied symlinks.
The same dereference behavior also affects `copy()` when the source path itself is a symlink.
### PoC
```js import { NodeHfs } from "@humanfs/node"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os";
const root = await fs.mkdtemp(path.join(os.tmpdir(), "humanfs-")); const src = path.join(root, "src"); const dst = path.join(root, "dst"); const secret = path.join(root, "secret.txt");
await fs.mkdir(src); await fs.writeFile(secret, "TOPSECRET"); await fs.symlink(secret, path.join(src, "link.txt"));
const hfs = new NodeHfs(); await hfs.copyAll(src, dst);
console.log(await fs.readFile(path.join(dst, "link.txt"), "utf8")); // TOPSECRET ```
### Impact
If an application uses `copyAll()` or `copy()` on attacker-controlled paths, a symlink can be used to pull arbitrary readable host files into the copied output. In practice, that can turn a normal workspace copy, export, or packaging step into a file disclosure primitive.
Patch / fix
- npm @humanfs/node 0.16.8
Is GHSA-p498-v437-472g in your dependencies? Fencer shows which of your repositories are affected as soon as an advisory lands, and opens the fix as a pull request.
Check with FencerCVSS scores
| assigned by | version | score | vector |
|---|---|---|---|
| the maintainers | 4.0 | 5.7 | CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N |
Affected packages
| ecosystem | package | affected | fixed in |
|---|---|---|---|
| npm | @humanfs/node | <0.16.8 |
0.16.8 |
Classification
- Weakness
-
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory
References
Sources
- GitHub Advisory GHSA-p498-v437-472g
- Maintainer advisory GHSA-p498-v437-472g
Fencer Intel keeps one entry per vulnerability: the CVE id wins, then GHSA, and records that alias each other are merged, except that two CVEs always keep separate entries. The same data is available as JSON.
The maintainers of humanwhocodes/humanfs published their own advisory on GitHub. Reviewed sources take precedence over it.