id: 43b943b9-4e27-4b4a-a7d2-550f9a1e99d5 title: Hostile .forge symlinks escape the repo and publish host files state: closed labels:
- kind:security assignees: [] milestone: null created: 2026-10-27T18:39:53.651563186Z updated: 2026-10-27T19:53:10.436268596Z
Problem
The .forge symlink refusal is incomplete. read_text_capped checks only the final file, but build readers follow a symlinked .forge root or section root, and artifact copy plus PR patch/CI log reads have no symlink check at all. A hostile repo can make hu13 build copy host files into the public site (including private inbox files, SSH keys, or CI secrets).
Verified vectors:
.forge->/tmp/secret-forge(external tree withissues/...andwiki/secret.md): build emitsissues/leak.htmlandwiki/secret.md.htmlcontaining the external text..forge/wiki->/tmp/secret-wiki: build emits the external markdown page..forge/releases/out/v1->/tmp/secret-dir:collect_artifacts/copy_treefollow the root symlink and copy/tmp/secret-dir/secret.txttodist/releases/v1/secret.txt(downloadable)..forge/prs/<uuid>/patch.diff->/tmp/secret.txt: build renders the external file as the PR patch..forge/ci/runs/<ts>/steps/00.log->/tmp/secret.log: build renders the external file as a CI log.
src/forge.rs:640 and src/forge.rs:842 use raw std::fs::read; src/releases.rs:127/181/476 start at the configured root; src/wiki.rs and src/releases.rs only reject entry-level symlinks.
Impact
Arbitrary local-file disclosure into dist/ when building an untrusted checkout. The same files can be served by serve/Pages. The patch and CI log reads are also unbounded, so a large .forge file can OOM the build even without a symlink.
Fix
- Reject a symlinked
.forge,.forge/wiki,.forge/releases,.forge/releases/out, and.forge/releases/out/<version>root (or resolve and require it under the repo). - Check symlinks for
patch.diffand every CI step log before reading; route them through a capped reader. - Add regression tests using symlinked roots and symlinked patch/log fixtures.