Commit c36365c522
c36365c522cfcbe5ea19a956543057218e41491e
parent: 34a78ef570
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
cmc <hello@cleberg.net> · 2026-09-05 06:03 UTC
wiki: fix raw-route climb-out, filter HasWiki by extension, drop stale claims
wikiRaw joined the request path onto .gitbay/wiki with path.Join,
which resolves .. rather than refusing it; a percent-encoded ..
reached the handler with real .. segments (the mux only cleans the
escaped path, not what PathValue decodes to). Guard it the same way
wiki show already guards page names.
HasWiki counted any blob under .gitbay/wiki, so a tree holding only
an image showed the tab while the page itself said there was no
wiki. Filter by the same page extensions wiki list and wiki show use.
The wiki page's "or use the file editor" hint overclaimed: repo
commit-file refuses repositories requiring signed commits, so the
editor does not work on every repository. Say push only. README.org
still described the wiki as a separate clonable git repository.
This repository's own heavy jobs (test, vuln, sonar) now skip on a
.gitbay/wiki-only push, so migrating the production wiki into this
storage does not itself queue the full suite. build stays
unconditional.
Ref #170
Layout: unified · split
.gitbay/ci.yml
+6
| @@ -15,6 +15,8 @@ jobs: |
| 15 | 15 | steps: |
| 16 | 16 | - missing=""; for t in git git-lfs gpg; do command -v "$t" >/dev/null || missing="$missing $t"; done; test -x /usr/sbin/sshd || missing="$missing sshd"; test -z "$missing" || { echo "runner is missing:$missing"; exit 1; } |
| 17 | 17 | - go test ./... -count=1 -timeout 20m |
| 18 | paths-ignore: |
| 19 | - .gitbay/wiki/** |
| 18 | 20 | # Vulnerability scanning, separate from test so a newly published advisory |
| 19 | 21 | # against unchanged code does not mask a real test failure. It fails the |
| 20 | 22 | # build on purpose: an advisory that only lands in a report nobody reads is |
| @@ -23,6 +25,8 @@ jobs: |
| 23 | 25 | vuln: |
| 24 | 26 | steps: |
| 25 | 27 | - go run golang.org/x/vuln/cmd/govulncheck@latest ./... |
| 28 | paths-ignore: |
| 29 | - .gitbay/wiki/** |
| 26 | 30 | # SonarCloud static analysis. Report-only: unlike vuln this does not |
| 27 | 31 | # gate, so a first scan of an existing codebase does not turn every |
| 28 | 32 | # build red before anyone has read what it says. Flip the trailing |
| @@ -66,3 +70,5 @@ jobs: |
| 66 | 70 | fi |
| 67 | 71 | SONAR_HOST_URL=https://sonarcloud.io \ |
| 68 | 72 | "$SCANNER/bin/sonar-scanner" -Dsonar.scm.revision="${GITBAY_SHA:-}" $BRANCH_ARG || true |
| 73 | paths-ignore: |
| 74 | - .gitbay/wiki/** |
README.org
+1 −2
| @@ -104,8 +104,7 @@ via =gitbay man=, completions via =gitbay completion <shell>=. |
| 104 | 104 | |
| 105 | 105 | * Documentation |
| 106 | 106 | |
| 107 | | Docs live in [[https://gitbay.org/krz/gitbay/wiki][the wiki]] — itself a git repository |
| 108 | | (=git clone ssh://git@gitbay.org/krz/gitbay.wiki.git=), dogfooding the |
| 107 | Docs live in [[https://gitbay.org/krz/gitbay/wiki][the wiki]], dogfooding the |
| 109 | 108 | wiki feature: |
| 110 | 109 | |
| 111 | 110 | - [[https://gitbay.org/krz/gitbay/wiki/Users][user guide]] — accounts, keys, verified commits, repos, issues, MRs, scripting |
e2e/wiki_test.go
+8
| @@ -47,6 +47,7 @@ func TestWikis(t *testing.T) { |
| 47 | 47 | "# welcome\n\nsee [Setup](Setup.md) and \n"), 0o644) |
| 48 | 48 | os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "Setup.org"), []byte("* setup\n\nsteps here\n"), 0o644) |
| 49 | 49 | os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "shot.png"), []byte{0x89, 0x50, 0x4e, 0x47}, 0o644) |
| 50 | os.WriteFile(filepath.Join(dir, "top.txt"), []byte("not part of the wiki\n"), 0o644) |
| 50 | 51 | mustGit(t, dir, env, "add", ".") |
| 51 | 52 | mustGit(t, dir, env, "commit", "-q", "-m", "wiki start") |
| 52 | 53 | mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app"), "main") |
| @@ -80,6 +81,13 @@ func TestWikis(t *testing.T) { |
| 80 | 81 | if status != 200 || !strings.HasPrefix(raw, "\x89PNG") { |
| 81 | 82 | t.Fatalf("wiki raw: %d", status) |
| 82 | 83 | } |
| 84 | // The raw route cannot climb out of .gitbay/wiki. A literal ".." is |
| 85 | // caught by the mux's own path cleaning, which would make this pass |
| 86 | // vacuously; percent-encoding it reaches the handler with real ".." |
| 87 | // segments in PathValue, which is what the guard has to refuse. |
| 88 | if status, body := inst.get(t, "/alice/app/wiki/_raw/%2e%2e/%2e%2e/top.txt"); status == 200 { |
| 89 | t.Fatalf("wiki raw escaped .gitbay/wiki: %d\n%s", status, body) |
| 90 | } |
| 83 | 91 | |
| 84 | 92 | // A wiki is readable from every surface, not just a browser: the |
| 85 | 93 | // commands are what the web dispatches, and what the CLI and the |
internal/httpd/wiki.go
+30 −4
| @@ -17,18 +17,39 @@ import ( |
| 17 | 17 | // wikiTreePath is where wiki pages live in a repository's tree. |
| 18 | 18 | const wikiTreePath = ".gitbay/wiki" |
| 19 | 19 | |
| 20 | // wikiExts are the page formats that count as wiki content. |
| 21 | var wikiExts = []string{".md", ".org", ".markdown"} |
| 22 | |
| 20 | 23 | // wikiDir returns repo's directory and default branch, where wiki pages |
| 21 | 24 | // are resolved from .gitbay/wiki. |
| 22 | 25 | func (s *Server) wikiDir(repo store.Repo) (dir, branch string) { |
| 23 | 26 | return control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name), repo.DefaultBranch |
| 24 | 27 | } |
| 25 | 28 | |
| 26 | | // hasWiki reports whether repo's default branch holds a non-empty |
| 27 | | // .gitbay/wiki tree. |
| 29 | // hasWiki reports whether repo's default branch holds a wiki page. |
| 28 | 30 | func (s *Server) hasWiki(repo store.Repo) bool { |
| 29 | 31 | dir, branch := s.wikiDir(repo) |
| 30 | 32 | entries, err := gitutil.ListTree(dir, branch, wikiTreePath) |
| 31 | | return err == nil && len(entries) > 0 |
| 33 | if err != nil { |
| 34 | return false |
| 35 | } |
| 36 | for _, e := range entries { |
| 37 | if e.Type == "blob" && wikiExtMatch(e.Name) { |
| 38 | return true |
| 39 | } |
| 40 | } |
| 41 | return false |
| 42 | } |
| 43 | |
| 44 | // wikiExtMatch reports whether name has one of the wiki page extensions. |
| 45 | func wikiExtMatch(name string) bool { |
| 46 | ext := strings.ToLower(path.Ext(name)) |
| 47 | for _, want := range wikiExts { |
| 48 | if ext == want { |
| 49 | return true |
| 50 | } |
| 51 | } |
| 52 | return false |
| 32 | 53 | } |
| 33 | 54 | |
| 34 | 55 | // wiki renders a page from the repo's .gitbay/wiki tree. The home page is |
| @@ -94,7 +115,12 @@ func (s *Server) wikiRaw(w http.ResponseWriter, r *http.Request) { |
| 94 | 115 | return |
| 95 | 116 | } |
| 96 | 117 | dir, branch := s.wikiDir(p.Repo) |
| 97 | | data, err := gitutil.ReadBlob(dir, branch, path.Join(wikiTreePath, strings.Trim(r.PathValue("path"), "/")), s.cfg.Limits.MaxBlobBytes) |
| 118 | rel := path.Join(wikiTreePath, strings.Trim(r.PathValue("path"), "/")) |
| 119 | if !strings.HasPrefix(rel, wikiTreePath+"/") { |
| 120 | s.notFound(w, r) |
| 121 | return |
| 122 | } |
| 123 | data, err := gitutil.ReadBlob(dir, branch, rel, s.cfg.Limits.MaxBlobBytes) |
| 98 | 124 | if err != nil { |
| 99 | 125 | s.notFound(w, r) |
| 100 | 126 | return |
internal/web/templates/wiki.html
+2 −2
| @@ -2,7 +2,7 @@ |
| 2 | 2 | {{define "content"}} |
| 3 | 3 | <h1 class="vh">Wiki{{if .Page}}: {{.Page}}{{end}}</h1> |
| 4 | 4 | {{if .Missing}}<p class="empty-note">no wiki yet — add pages under <code>.gitbay/wiki/</code> on the default branch:<br> |
| 5 | | push <code>.gitbay/wiki/Home.md</code> (or .org), or use the file editor.</p> |
| 5 | push <code>.gitbay/wiki/Home.md</code> (or .org).</p> |
| 6 | 6 | {{else}} |
| 7 | 7 | <div class="wikilayout"> |
| 8 | 8 | <div class="wikipage"> |
| @@ -14,7 +14,7 @@ push <code>.gitbay/wiki/Home.md</code> (or .org), or use the file editor.</p> |
| 14 | 14 | <nav class="wikinav"> |
| 15 | 15 | <p class="meta">pages</p> |
| 16 | 16 | <ul>{{range .Pages}}<li><a {{if eq . $.Page}}class="active" {{end}}href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/wiki/{{.}}">{{.}}</a></li>{{end}}</ul> |
| 17 | | <p class="meta">edit under <code>.gitbay/wiki/</code> — push, or the file editor.</p> |
| 17 | <p class="meta">edit under <code>.gitbay/wiki/</code> by push.</p> |
| 18 | 18 | </nav> |
| 19 | 19 | </div> |
| 20 | 20 | {{end}} |