What
internal/core/site/build.go:8-10 says nothing writes outside the output directory: every write goes through os.Root at dest. Reads do not get the matching containment.
loadPage (internal/core/site/compose.go:88) is:
fsutil.ReadGuarded(joinRepo(c.repoRoot, rel), maxPageBytes)
ReadGuarded is leaf O_NOFOLLOW only. A committed directory symlink (docs/explanation -> ../../.ssh, git mode 120000) is a valid relative path: ValidRelPath accepts it, Join walks through the ancestor, ReadGuarded opens the leaf inside the target, and the bytes can be composed into published HTML.
The same joinRepo + ReadGuarded shape is in explorer.go:130, assets.go (raster/SVG copy), recordpage.go:121, foundations.go:75.
Positioning already treats this as the attack (internal/core/positioning/containment_test.go:17-25) and uses ReadGuardedInRoot. Site did not.
Suggested fix
Open os.Root at the repo root for site reads and use ReadGuardedInRoot (or equivalent) for every joinRepo content read. Port or share positioning's hostile-repo fixture (absolute etclink -> /etc and relative home -> ../../..). Assert the containment canary never appears in a composed page.
Happy to send a PR once this is accepted.
What
internal/core/site/build.go:8-10says nothing writes outside the output directory: every write goes throughos.Rootat dest. Reads do not get the matching containment.loadPage(internal/core/site/compose.go:88) is:ReadGuardedis leafO_NOFOLLOWonly. A committed directory symlink (docs/explanation -> ../../.ssh, git mode 120000) is a valid relative path:ValidRelPathaccepts it,Joinwalks through the ancestor,ReadGuardedopens the leaf inside the target, and the bytes can be composed into published HTML.The same
joinRepo+ReadGuardedshape is inexplorer.go:130,assets.go(raster/SVG copy),recordpage.go:121,foundations.go:75.Positioning already treats this as the attack (
internal/core/positioning/containment_test.go:17-25) and usesReadGuardedInRoot. Site did not.Suggested fix
Open
os.Rootat the repo root for site reads and useReadGuardedInRoot(or equivalent) for everyjoinRepocontent read. Port or share positioning's hostile-repo fixture (absoluteetclink -> /etcand relativehome -> ../../..). Assert the containment canary never appears in a composed page.Happy to send a PR once this is accepted.