{"id":"GHSA-p498-v437-472g","summary":"humanfs: Recursive copy follows symlinked files and copies data from outside the source tree","details":"### Summary\n\n`@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.\n\n### Details\n\nThe 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()`.\n\nOn 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.\n\nThat 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.\n\nThe same dereference behavior also affects `copy()` when the source path itself is a symlink.\n\n### PoC\n\n```js\nimport { NodeHfs } from \"@humanfs/node\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nconst root = await fs.mkdtemp(path.join(os.tmpdir(), \"humanfs-\"));\nconst src = path.join(root, \"src\");\nconst dst = path.join(root, \"dst\");\nconst secret = path.join(root, \"secret.txt\");\n\nawait fs.mkdir(src);\nawait fs.writeFile(secret, \"TOPSECRET\");\nawait fs.symlink(secret, path.join(src, \"link.txt\"));\n\nconst hfs = new NodeHfs();\nawait hfs.copyAll(src, dst);\n\nconsole.log(await fs.readFile(path.join(dst, \"link.txt\"), \"utf8\"));\n// TOPSECRET\n```\n\n### Impact\n\nIf 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.","modified":"2026-09-10T03:50:54.516902811Z","published":"2026-09-02T14:27:16Z","database_specific":{"cwe_ids":["CWE-22"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-09-02T14:27:16Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/humanwhocodes/humanfs/security/advisories/GHSA-p498-v437-472g"},{"type":"WEB","url":"https://github.com/humanwhocodes/humanfs/commit/22bbaa4487a3e6c1197ca619840de4615d0c3404"},{"type":"PACKAGE","url":"https://github.com/humanwhocodes/humanfs"},{"type":"WEB","url":"https://github.com/humanwhocodes/humanfs/releases/tag/node-v0.16.8"}],"affected":[{"package":{"name":"@humanfs/node","ecosystem":"npm","purl":"pkg:npm/%40humanfs/node"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.16.8"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-p498-v437-472g/GHSA-p498-v437-472g.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"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"}]}