diff options
| author | Lena <lena@omega> | 2026-08-01 00:00:00 +0000 |
|---|---|---|
| committer | Lena <lena@omega> | 2026-08-01 00:00:00 +0000 |
| commit | bb6bc2387d58c737dbf92e73f8544bf39319dbd8 (patch) | |
| tree | 6d5eaf5dac213074cd832da50f64260fce728fed /fuzz_test.go | |
| download | git-forge-master.tar.gz | |
A small self-hosted git forge: HTTP push and pull, cgit browsing,
local and OIDC accounts, TOTP, scoped push tokens, per-repo
permissions, issues, a wiki, and outbound mail.
It does not reimplement git. git-http-backend and cgit run as CGI
behind its own listener, which authorises every request before
exec'ing either. One binary, one data directory, one listener: no
database, no JavaScript, and state is JSON in a git repo with a
working tree.
Diffstat (limited to 'fuzz_test.go')
| -rw-r--r-- | fuzz_test.go | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/fuzz_test.go b/fuzz_test.go new file mode 100644 index 0000000..866863d --- /dev/null +++ b/fuzz_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "path/filepath" + "slices" + "strings" + "testing" + "time" +) + +// The parsers that are handed bytes from outside: an id token, a claim, a URL +// path, and a secret typed into a form. Each target states the property the +// caller relies on, so the seed corpus runs with the rest of the tests and a +// longer search is one flag away. + +// splitJWT takes a token apart before anything is known about it, and +// verifySignature is then handed whatever came out. +func FuzzSplitJWT(f *testing.F) { + f.Add("aGVhZGVy.cGF5bG9hZA.c2ln") + f.Add("...") + f.Add("") + f.Fuzz(func(t *testing.T, token string) { + signed, sig, kid, alg, _, err := splitJWT(token) + if err != nil { + return + } + if verifySignature(jwk{Kty: "RSA", N: kid, E: "AQAB"}, alg, signed, sig) == nil { + t.Fatalf("a made up rsa key verified %q", token) + } + if verifySignature(jwk{Kty: "EC", Crv: "P-256", X: kid, Y: kid}, alg, signed, sig) == nil { + t.Fatalf("a made up ec key verified %q", token) + } + }) +} + +// The name becomes a key in repos.json and a component of a URL, out of a +// claim from somewhere else. +func FuzzSanitizeName(f *testing.F) { + f.Add("Carol") + f.Add("../../config") + f.Fuzz(func(t *testing.T, in string) { + if out := sanitizeName(in); out != "" && !localNameRE.MatchString(out) { + t.Fatalf("sanitizeName(%q) = %q, which is not a usable account name", in, out) + } + }) +} + +// Whatever this lets through is handed to both CGIs in an environment +// variable. +func FuzzServablePath(f *testing.F) { + f.Add("/notes/log/") + f.Add("/a/../b") + f.Fuzz(func(t *testing.T, p string) { + if !servablePath(p) { + return + } + if hasControlByte(p) { + t.Fatalf("servablePath(%q) accepted a control byte", p) + } + parts := strings.Split(strings.TrimPrefix(p, "/"), "/") + for i, part := range parts { + if part == "." || part == ".." || (part == "" && i < len(parts)-1) { + t.Fatalf("servablePath(%q) accepted a %q component", p, part) + } + } + }) +} + +// Whatever this lets through is joined onto the repos directory, for the +// repository and for both of its siblings. +func FuzzValidRepoPath(f *testing.F) { + f.Add("infra/dns") + f.Add("../etc") + f.Fuzz(func(t *testing.T, p string) { + if !validRepoPath(p) { + return + } + for _, suffix := range []string{".git", ".issues.git", ".wiki.git"} { + joined := filepath.Join("/repos", p+suffix) + if joined != filepath.Clean(joined) || !strings.HasPrefix(joined, "/repos/") { + t.Fatalf("validRepoPath(%q) accepted a path that lands on %q", p, joined) + } + if strings.Count(strings.TrimPrefix(joined, "/repos/"), "/") > 1 { + t.Fatalf("validRepoPath(%q) accepted more than one section deep", p) + } + } + }) +} + +// A page listed in the index has to open the file it was listed from. A push +// can put any name in the tree. +func FuzzWikiNameRoundTrip(f *testing.F) { + f.Add("docs/index.md") + f.Add("myindex.md") + f.Add(".md") + f.Fuzz(func(t *testing.T, file string) { + page := wikiPage(file) + files, ok := wikiFiles(page) + if !ok || !slices.Contains(files, file) { + return // not listed, so never linked to + } + if wikiPage(files[0]) != page { + t.Fatalf("%q is listed as %q, which reads back as %q", file, page, wikiPage(files[0])) + } + }) +} + +// The secret being enrolled arrives in a form, so this decodes whatever was +// typed. +func FuzzTOTPSecret(f *testing.F) { + f.Add("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ") + f.Add("!!!") + f.Fuzz(func(t *testing.T, secret string) { + code := totpCode(secret, time.Unix(1234567890, 0)) + if code != "" && len(code) != totpDigits { + t.Fatalf("totpCode(%q) = %q", secret, code) + } + if code == "" && totpStep(secret, "", time.Unix(1234567890, 0)) >= 0 { + t.Fatalf("an unreadable secret accepted an empty code") + } + }) +} |