file review #5 issues #106-#114
Message
file review #5 issues #106-#114
Diff
.forge/issues/0572a4d9-8185-4d2d-b815-0d49e5f07c8c/issue.md+26 −0
--- /dev/null+++ b/.forge/issues/0572a4d9-8185-4d2d-b815-0d49e5f07c8c/issue.md@@ -0,0 +1,26 @@1+---2+id: 0572a4d9-8185-4d2d-b815-0d49e5f07c8c3+title: Agents guide claims checkbox state and body hash are stored, but they are not4+state: open5+labels:6+- kind:docs7+assignees: []8+milestone: null9+created: 2026-10-27T18:40:33.033067153Z10+updated: null11+---12+## Problem13+14+The generated agents guide promises storage fields that do not exist. `src/agents.rs:64` says:15+16+> Stored per submission: license version + checkbox state + body hash + timestamp.17+18+Actual `store_submission` JSON (`src/inbox.rs`) stores `type`, `title`, `body`, `handle`, `contact`, `license_version`, `received_at`, `salted_ip_hash` — no checkbox state and no body hash. The Pages Function stores the same fields.19+20+## Impact21+22+Agents and users are told a retention/provenance contract the inbox does not implement. This is an accuracy/privacy documentation bug.23+24+## Fix25+26+Either store the promised fields (and document their exact meaning) or change the guide to the actual stored shape. Keep both Docker and Pages writers in sync.
.forge/issues/31863f16-5f76-479b-be32-61b1107441a6/issue.md+41 −0
--- /dev/null+++ b/.forge/issues/31863f16-5f76-479b-be32-61b1107441a6/issue.md@@ -0,0 +1,41 @@1+---2+id: 31863f16-5f76-479b-be32-61b1107441a63+title: Cloudflare Pages clean-URL redirects break the one-shape canonical URL contract4+state: open5+labels:6+- area:render7+assignees: []8+milestone: null9+created: 2026-10-27T18:40:05.827762324Z10+updated: null11+---12+## Problem13+14+Cloudflare Pages rewrites HTML routes to extensionless URLs, while `hu13 build` emits `.html` links and `hu13 serve`/nginx serve them directly. The `dist/` is therefore not one canonical shape: every generated page link redirects on Pages.15+16+## Verified reproduction17+18+Wrangler 4.154.0 `wrangler pages dev dist`:19+20+```21+GET /issues/index.html -> 308 Location: /issues/22+GET /files/a.txt.html -> 308 Location: /files/a.txt23+GET /raw/evil.html -> 308 Location: /raw/evil24+GET /releases/v1.html -> 308 Location: /releases/v125+GET /releases/v1/evil.html -> 308 Location: /releases/v1/evil26+```27+28+`GET /issues/`, `/files/a.txt`, `/raw/evil`, and `/releases/v1` then return 200. A `_redirects` rule with status 200 cannot override the clean-URL redirect (verified).29+30+This also makes the artifact redirect path security-relevant: #106 is the actual active-content bug, and the redirect is what changes the final URL.31+32+## Impact33+34+- Internal navigation and feed links take a 308 on Pages only.35+- Raw HTML downloads redirect on Pages only.36+- External canonical URLs (`/issues/index.html`) are not the Pages canonical URL.37+- The one-shape requirement is not met even though pages remain reachable.38+39+## Fix options40+41+Pick during convergence: emit extensionless canonical links and adapt `serve`/docs, or add a Pages-side function/worker that preserves `.html`, or explicitly declare Pages redirect behavior acceptable and document the canonical URL difference. Do not rely on `_redirects` to suppress the platform redirect.
.forge/issues/43b943b9-4e27-4b4a-a7d2-550f9a1e99d5/issue.md+34 −0
--- /dev/null+++ b/.forge/issues/43b943b9-4e27-4b4a-a7d2-550f9a1e99d5/issue.md@@ -0,0 +1,34 @@1+---2+id: 43b943b9-4e27-4b4a-a7d2-550f9a1e99d53+title: Hostile .forge symlinks escape the repo and publish host files4+state: open5+labels:6+- kind:security7+assignees: []8+milestone: null9+created: 2026-10-27T18:39:53.651563186Z10+updated: null11+---12+## Problem13+14+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).15+16+Verified vectors:17+18+1. `.forge` -> `/tmp/secret-forge` (external tree with `issues/...` and `wiki/secret.md`): build emits `issues/leak.html` and `wiki/secret.md.html` containing the external text.19+2. `.forge/wiki` -> `/tmp/secret-wiki`: build emits the external markdown page.20+3. `.forge/releases/out/v1` -> `/tmp/secret-dir`: `collect_artifacts`/`copy_tree` follow the root symlink and copy `/tmp/secret-dir/secret.txt` to `dist/releases/v1/secret.txt` (downloadable).21+4. `.forge/prs/<uuid>/patch.diff` -> `/tmp/secret.txt`: build renders the external file as the PR patch.22+5. `.forge/ci/runs/<ts>/steps/00.log` -> `/tmp/secret.log`: build renders the external file as a CI log.23+24+`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.25+26+## Impact27+28+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.29+30+## Fix31+32+- 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).33+- Check symlinks for `patch.diff` and every CI step log before reading; route them through a capped reader.34+- Add regression tests using symlinked roots and symlinked patch/log fixtures.
.forge/issues/567552a1-389b-4856-9e3e-1b320d5f1095/issue.md+38 −0
--- /dev/null+++ b/.forge/issues/567552a1-389b-4856-9e3e-1b320d5f1095/issue.md@@ -0,0 +1,38 @@1+---2+id: 567552a1-389b-4856-9e3e-1b320d5f10953+title: Pages artifact download policy is invalid (two splats), allowing active HTML/SVG4+state: open5+labels:6+- kind:security7+assignees: []8+milestone: null9+created: 2026-10-27T18:39:46.325820585Z10+updated: null11+---12+## Problem13+14+The Pages artifact download policy is invalid: Cloudflare `_headers` allows only one `*` wildcard per rule, but `HEADERS_FILE` emits `/releases/*/*`. Wrangler reports it invalid and skips it, so release artifacts are served as active same-origin HTML/SVG instead of downloads. This means f0rge #97 is still open on Pages even though `hu13 serve` is fixed.15+16+## Verified reproduction17+18+`wrangler pages dev` 4.154.0 over a dist with `.forge/releases/out/v1/evil.html` and `evil.svg`:19+20+```21+Found 1 invalid header rule:22+ Only one wildcard is allowed per rule. Use a named placeholder (e.g. :project) instead. Skipping /releases/*/*.23+ at dist/_headers:7 | /releases/*/*24+```25+26+Then:27+28+```29+GET /releases/v1/evil.svg -> 200 image/svg+xml, no Content-Disposition30+GET /releases/v1/evil.html -> 308 Location: /releases/v1/evil31+GET /releases/v1/evil -> 200 text/html, no Content-Disposition32+```33+34+Both artifact bodies are script-bearing and execute in the inbox origin. `src/build.rs:1157`, `docs/cloudflare.md:94`, and `.forge/wiki/deploy:69` all claim the artifact download policy exists.35+36+## Fix candidate37+38+Use `/releases/:version/*` (one splat plus a placeholder). Verified with Wrangler: it parses as valid, applies `Content-Disposition: attachment` to extensionless artifact URLs and artifact directory indexes, and leaves `/releases/v1` release pages inline. Add a real Wrangler/Pages header test instead of only checking the string.
.forge/issues/7d4b2e5a-09b9-4d6d-bb2e-1aeabb3c5a45/issue.md+37 −0
--- /dev/null+++ b/.forge/issues/7d4b2e5a-09b9-4d6d-bb2e-1aeabb3c5a45/issue.md@@ -0,0 +1,37 @@1+---2+id: 7d4b2e5a-09b9-4d6d-bb2e-1aeabb3c5a453+title: parse_ts_secs panics on multibyte timestamps4+state: open5+labels:6+- kind:build7+assignees: []8+milestone: null9+created: 2026-10-27T18:39:58.917947713Z10+updated: null11+---12+## Problem13+14+`parse_ts_secs` slices the offset by byte index without checking a char boundary. A hostile but valid UTF-8 `.forge` timestamp can panic the whole build. `src/build.rs:599`.15+16+## Verified reproduction17+18+`.forge/releases/v1.md`:19+20+```yaml21+created: "2026-01-01 12:34:56+€!"22+```23+24+Build:25+26+```27+cargo run -- build /tmp/rev5-ts -o /tmp/rev5-ts-out28+exit=10129+thread 'main' panicked at src/build.rs:599:27:30+end byte index 11 is not a char boundary; it is inside '€' (bytes 9..12 of string)31+```32+33+`created`/`updated` from issues, PRs, comments, reviews, releases, and CI runs all go through `time_ago` -> `parse_ts_secs`. This is the same class as #99 (multibyte truncation) but in a different parser.34+35+## Fix36+37+Require the time/offset suffix to be ASCII before slicing, or guard each `str` slice with `is_char_boundary` and return `None`. Add a multibyte timestamp regression test using the repro above.
.forge/issues/8882516a-8e3e-4197-978e-c3718eaf3854/issue.md+33 −0
--- /dev/null+++ b/.forge/issues/8882516a-8e3e-4197-978e-c3718eaf3854/issue.md@@ -0,0 +1,33 @@1+---2+id: 8882516a-8e3e-4197-978e-c3718eaf38543+title: Relative markdown links/images and heading anchors 404 in generated pages4+state: open5+labels:6+- area:render7+assignees: []8+milestone: null9+created: 2026-10-27T18:40:23.190333659Z10+updated: null11+---12+## Problem13+14+Markdown links and images are rendered verbatim relative to the generated page. They point at source paths that do not exist in `dist/`, so README, file-page, wiki, release, issue, and PR links/images fail.15+16+Verified fixture:17+18+```19+README.md: [guide](docs/guide.md)20+index.html: href="docs/guide.md" -> /docs/guide.md (404; page is /files/docs/guide.md.html, raw is /raw/docs/guide.md)21+files/docs/guide.md.html: [peer](peer.md) -> /files/docs/peer.md (404; page is /files/docs/peer.md.html)22+wiki/home.md.html: [other](other.md) -> /wiki/other.md (404; page is /wiki/other.md.html)23+```24+25+Headings are emitted without `id`s, so README/wiki table-of-contents `#section` links also do not resolve.26+27+## Impact28+29+Primary README navigation, wiki navigation, and relative images are broken on every target, including `serve` and Pages. This is core documentation UX, not just cosmetics.30+31+## Fix32+33+Rewrite relative markdown destinations/images against the current page context (README root, file path, wiki path, release path; `/files/...html`, `/raw/...`, `/wiki/...html` as appropriate), or explicitly document that only absolute/raw links work. Add heading IDs if anchor navigation is expected. Add fixtures covering README, nested file page, wiki, and release body links.
.forge/issues/9c2726a2-8e2b-47cf-9a09-94b13aa491e4/issue.md+31 −0
--- /dev/null+++ b/.forge/issues/9c2726a2-8e2b-47cf-9a09-94b13aa491e4/issue.md@@ -0,0 +1,31 @@1+---2+id: 9c2726a2-8e2b-47cf-9a09-94b13aa491e43+title: serve 404.html symlink escapes www and leaks host files4+state: open5+labels:6+- kind:security7+assignees: []8+milestone: null9+created: 2026-10-27T18:40:16.931258254Z10+updated: null11+---12+## Problem13+14+`serve`'s normal static path resolves symlinks and rejects targets outside `www`, but the custom 404 fallback reads `www/404.html` with a raw `metadata`/`File::open` and no symlink or prefix check. A symlinked `404.html` can publish any host file on every unknown path.15+16+Verified:17+18+```19+ln -s /etc/hostname /tmp/rev5-404www/404.html20+hu13 serve --www /tmp/rev5-404www --inbox-dir /tmp/rev5-inbox --listen 127.0.0.1:3944121+curl http://127.0.0.1:39441/nope22+HTTP/1.1 404 Not Found23+Content-Type: text/html; charset=utf-824+reflectivity25+```26+27+If the symlink target is script-bearing HTML, it also executes in the inbox origin because the 404 response has no attachment disposition. `src/serve.rs:913-931`.28+29+## Fix30+31+Resolve `404.html` with the same `resolve_existing`/`starts_with(www)` check as normal static paths and reject symlinks (or require the resolved target under `www`). Add a symlinked-404 integration test.
.forge/issues/9c980dd0-e76b-4ff9-876f-40eec632d434/issue.md+30 −0
--- /dev/null+++ b/.forge/issues/9c980dd0-e76b-4ff9-876f-40eec632d434/issue.md@@ -0,0 +1,30 @@1+---2+id: 9c980dd0-e76b-4ff9-876f-40eec632d4343+title: Unknown top-level subcommand exits 04+state: open5+labels:6+- kind:build7+assignees: []8+milestone: null9+created: 2026-10-27T18:40:27.937158000Z10+updated: null11+---12+## Problem13+14+An unknown top-level subcommand prints an error and help, then exits 0. Scripts that typo `build`/`serve` continue as if the command succeeded.15+16+Verified:17+18+```19+$ hu13 nope20+hu13: unknown subcommand 'nope'21+...22+$ echo $?23+024+```25+26+`src/main.rs:493-497`. Unknown *options* and unknown inbox subcommands correctly return 2, so this is inconsistent.27+28+## Fix29+30+Return 2 from the unknown-subcommand arm; add a CLI exit-code test.
.forge/issues/ebaa1e1e-c386-49b4-92e2-8118a61f9175/issue.md+29 −0
--- /dev/null+++ b/.forge/issues/ebaa1e1e-c386-49b4-92e2-8118a61f9175/issue.md@@ -0,0 +1,29 @@1+---2+id: ebaa1e1e-c386-49b4-92e2-8118a61f91753+title: Reserved index names can overwrite section indexes (releases/issues/pulls/wiki)4+state: open5+labels:6+- area:render7+assignees: []8+milestone: null9+created: 2026-10-27T18:40:11.379047814Z10+updated: null11+---12+## Problem13+14+Generated index filenames are not reserved. A hostile `.forge` can overwrite the section index with a detail page:15+16+- release `version: index` writes `releases/index.html` (the release list) after the index and replaces it with the release page;17+- issue `id: index` writes `issues/index.html` after the issues index;18+- PR `id: index` writes `pulls/index.html` after the pulls index;19+- wiki file `index` (no extension) writes `wiki/index.html` after the wiki index.20+21+All four are accepted by the current `is_safe_segment`/`is_safe_id` checks. Verified with fixtures; e.g. a release named `index` makes `releases/index.html` render `<h1>Index release</h1>` instead of the release table.22+23+## Impact24+25+Hostile or accidental metadata silently destroys a section index, its navigation target, and the API/HTML consistency around it. No script injection, but a denial/correctness bug in the generated site.26+27+## Fix28+29+Reserve generated names (`index`) for releases, issue/PR ids, and wiki pages, or write indexes last and reject collisions loudly. Add one fixture covering all four collisions.