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:
1515 steps:
1616 - 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; }
1717 - go test ./... -count=1 -timeout 20m
18 paths-ignore:
19 - .gitbay/wiki/**
1820 # Vulnerability scanning, separate from test so a newly published advisory
1921 # against unchanged code does not mask a real test failure. It fails the
2022 # build on purpose: an advisory that only lands in a report nobody reads is
@@ -23,6 +25,8 @@ jobs:
2325 vuln:
2426 steps:
2527 - go run golang.org/x/vuln/cmd/govulncheck@latest ./...
28 paths-ignore:
29 - .gitbay/wiki/**
2630 # SonarCloud static analysis. Report-only: unlike vuln this does not
2731 # gate, so a first scan of an existing codebase does not turn every
2832 # build red before anyone has read what it says. Flip the trailing
@@ -66,3 +70,5 @@ jobs:
6670 fi
6771 SONAR_HOST_URL=https://sonarcloud.io \
6872 "$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>=.
104104
105105* Documentation
106106
107Docs 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
107Docs live in [[https://gitbay.org/krz/gitbay/wiki][the wiki]], dogfooding the
109108wiki feature:
110109
111110- [[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) {
4747 "# welcome\n\nsee [Setup](Setup.md) and ![shot](shot.png)\n"), 0o644)
4848 os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "Setup.org"), []byte("* setup\n\nsteps here\n"), 0o644)
4949 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)
5051 mustGit(t, dir, env, "add", ".")
5152 mustGit(t, dir, env, "commit", "-q", "-m", "wiki start")
5253 mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app"), "main")
@@ -80,6 +81,13 @@ func TestWikis(t *testing.T) {
8081 if status != 200 || !strings.HasPrefix(raw, "\x89PNG") {
8182 t.Fatalf("wiki raw: %d", status)
8283 }
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 }
8391
8492 // A wiki is readable from every surface, not just a browser: the
8593 // commands are what the web dispatches, and what the CLI and the
internal/httpd/wiki.go +30 −4
@@ -17,18 +17,39 @@ import (
1717// wikiTreePath is where wiki pages live in a repository's tree.
1818const wikiTreePath = ".gitbay/wiki"
1919
20// wikiExts are the page formats that count as wiki content.
21var wikiExts = []string{".md", ".org", ".markdown"}
22
2023// wikiDir returns repo's directory and default branch, where wiki pages
2124// are resolved from .gitbay/wiki.
2225func (s *Server) wikiDir(repo store.Repo) (dir, branch string) {
2326 return control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name), repo.DefaultBranch
2427}
2528
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.
2830func (s *Server) hasWiki(repo store.Repo) bool {
2931 dir, branch := s.wikiDir(repo)
3032 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.
45func 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
3253}
3354
3455// 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) {
94115 return
95116 }
96117 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)
98124 if err != nil {
99125 s.notFound(w, r)
100126 return
internal/web/templates/wiki.html +2 −2
@@ -2,7 +2,7 @@
22{{define "content"}}
33<h1 class="vh">Wiki{{if .Page}}: {{.Page}}{{end}}</h1>
44{{if .Missing}}<p class="empty-note">no wiki yet — add pages under <code>.gitbay/wiki/</code> on the default branch:<br>
5push <code>.gitbay/wiki/Home.md</code> (or .org), or use the file editor.</p>
5push <code>.gitbay/wiki/Home.md</code> (or .org).</p>
66{{else}}
77<div class="wikilayout">
88<div class="wikipage">
@@ -14,7 +14,7 @@ push <code>.gitbay/wiki/Home.md</code> (or .org), or use the file editor.</p>
1414<nav class="wikinav">
1515<p class="meta">pages</p>
1616<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>
1818</nav>
1919</div>
2020{{end}}