Skip to content
Fencer Intel
medium 5.7

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

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 Fencer

CVSS 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

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.