Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/ssh/fuzz_test.go

Line attribution on main · 123 line(s)

16bf0ebf
Keenan Nemetz
10d ago
1 package ssh
2
3 import (
4 "strings"
5 "testing"
6
d37d301f
Keenan Nemetz
15h ago
7 "rodent.software/vole/forge/repo"
16bf0ebf
Keenan Nemetz
10d ago
8 )
9
10 // FuzzSplitCommand drives the parser that turns an exec line from an
11 // unauthenticated connection into the argv the server acts on. The seeds are the
12 // shapes git actually sends, so `go test ./...` keeps refusing those; the
13 // fuzzer's job is everything between them.
14 //
15 // Two properties are checked, and neither is "it does not panic", which is the
16 // floor rather than the point:
17 //
18 // - It cannot invent bytes. Every byte of every argument came from the command
19 // line: a quote is consumed and writes nothing, an escape is consumed and
20 // writes one byte.
21 // - What it produces survives being written back out. The client builds these
22 // command lines, and the server parses them; if a value does not round-trip
23 // through git's quoting, one end misreads the other.
24 func FuzzSplitCommand(f *testing.F) {
25 for _, seed := range []string{
26 "git-upload-pack '/demo.git'",
27 `git-upload-pack "/demo.git"`,
28 "git-receive-pack demo.git",
29 "git-upload-pack '/unterminated",
30 `git-upload-pack "half\`,
31 "git-upload-pack\t'demo.git'",
32 "git-upload-pack 'demo.git'",
33 "''",
34 "",
35 "vole-runner",
36 "vole-change-merge demo 01M5XCHRQFT0FTM0CJYRYWVD3V",
37 `git-upload-pack '/it'\''s.git'`,
38 } {
39 f.Add(seed)
40 }
41 f.Fuzz(func(t *testing.T, command string) {
42 argv, err := splitCommand(command)
43 if err != nil {
44 return
45 }
46 if total := argBytes(argv); total > len(command) {
47 t.Fatalf("splitCommand(%q) produced %d bytes of arguments from %d bytes", command, total, len(command))
48 }
49 quoted := make([]string, len(argv))
50 for i, arg := range argv {
51 quoted[i] = quoteArg(arg)
52 }
53 command2 := strings.Join(quoted, " ")
54 again, err := splitCommand(command2)
55 if err != nil {
56 t.Fatalf("re-quoted %q (from %q) does not parse: %v", command2, command, err)
57 }
58 if len(again) != len(argv) {
59 t.Fatalf("%q split to %d arguments, but %q splits to %d", command, len(argv), command2, len(again))
60 }
61 for i := range argv {
62 if again[i] != argv[i] {
63 t.Fatalf("%q: argument %d is %q, but %q gives %q", command, i, argv[i], command2, again[i])
64 }
65 }
66 })
67 }
68
69 func argBytes(argv []string) int {
70 total := 0
71 for _, arg := range argv {
72 total += len(arg)
73 }
74 return total
75 }
76
77 // quoteArg writes one argument the way git does: single quoted, with an embedded
78 // quote closing the quoting, escaping itself, and reopening it.
79 func quoteArg(arg string) string {
80 return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'"
81 }
82
83 // FuzzRepoName drives the step after parsing: the argument becomes a repository
84 // name, not a path. It is the same untrusted text, so it belongs with the parser
85 // that produces it — fuzzing one and not the other leaves the decision that
86 // actually touches storage untested.
87 func FuzzRepoName(f *testing.F) {
88 for _, seed := range []string{
89 "/demo.git",
90 "demo.git",
91 "/demo",
92 "../demo.git",
93 "/a/b.git",
94 "/.git",
95 "/demo.git/",
96 "//demo.git",
97 "/demo.git/extra",
98 "/..git",
99 "/.a.git",
100 "",
101 } {
102 f.Add(seed)
103 }
104 f.Fuzz(func(t *testing.T, arg string) {
105 name, ok := repoName(arg)
106 if !ok {
107 return
108 }
109 // A name is one segment of a path under the repositories directory, so
110 // nothing that could climb or descend may survive as a name.
111 if strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") {
112 t.Fatalf("repoName(%q) = %q, which is a path rather than a name", arg, name)
113 }
114 if err := repo.ValidateName(name); err != nil {
115 t.Fatalf("repoName(%q) = %q, which the repository store refuses: %v", arg, name, err)
116 }
117 // The name has to be the one git sends back for it.
118 again, ok := repoName("/" + name + ".git")
119 if !ok || again != name {
120 t.Fatalf("repoName(%q) = %q, but \"/%s.git\" gives %q, %v", arg, name, name, again, ok)
121 }
122 })
123 }