Skip to content

Commit d8dac7f

Browse files
committed
core: add stricter route parsing
Move the route parsing done in 'bundle-server.go' to a new method 'core.ParseRoute'. In addition to the removal of excess path separators the method already does, limit the characters in each segment of the route (owner, repo, filename) to alphanumeric characters plus '-', '_', and '.'. Also, explicitly block '.' and '..' path components. These measures are added to prevent serving content from arbitrary relative paths in the bundle server. If less stringent path validation is desired in the future, the route parsing can be updated accordingly. Update all functions that use a user-supplied route ('bundleWebServer.serve', 'repoProvider.CreateRepository', 'repoProvider.RemoveRoute') to use the new parsing function. Signed-off-by: Victoria Dye <vdye@github.com>
1 parent 4f00efc commit d8dac7f

File tree

4 files changed

+157
-18
lines changed

4 files changed

+157
-18
lines changed

cmd/git-bundle-web-server/bundle-server.go

+1-18
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"os"
1010
"os/signal"
1111
"path/filepath"
12-
"strings"
1312
"sync"
1413
"syscall"
1514
"time"
@@ -81,30 +80,14 @@ func NewBundleWebServer(logger log.TraceLogger,
8180
return bundleServer, nil
8281
}
8382

84-
func (b *bundleWebServer) parseRoute(ctx context.Context, path string) (string, string, string, error) {
85-
elements := strings.FieldsFunc(path, func(char rune) bool { return char == '/' })
86-
switch len(elements) {
87-
case 0:
88-
return "", "", "", fmt.Errorf("empty route")
89-
case 1:
90-
return "", "", "", fmt.Errorf("route has owner, but no repo")
91-
case 2:
92-
return elements[0], elements[1], "", nil
93-
case 3:
94-
return elements[0], elements[1], elements[2], nil
95-
default:
96-
return "", "", "", fmt.Errorf("path has depth exceeding three")
97-
}
98-
}
99-
10083
func (b *bundleWebServer) serve(w http.ResponseWriter, r *http.Request) {
10184
ctx := r.Context()
10285

10386
ctx, exitRegion := b.logger.Region(ctx, "http", "serve")
10487
defer exitRegion()
10588

10689
path := r.URL.Path
107-
owner, repo, filename, err := b.parseRoute(ctx, path)
90+
owner, repo, filename, err := core.ParseRoute(path, false)
10891
if err != nil {
10992
w.WriteHeader(http.StatusNotFound)
11093
fmt.Printf("Failed to parse route: %s\n", err)

internal/core/funcs.go

+31
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package core
22

33
import (
4+
"fmt"
45
"regexp"
56
"strings"
67
)
@@ -27,3 +28,33 @@ func GetRouteFromUrl(url string) (string, bool) {
2728

2829
return "", false
2930
}
31+
32+
func ParseRoute(route string, repoOnly bool) (string, string, string, error) {
33+
elements := strings.FieldsFunc(route, func(char rune) bool { return char == '/' })
34+
validElementPattern := regexp.MustCompile(`^[\w\.-]+$`)
35+
for _, e := range elements {
36+
if !validElementPattern.MatchString(e) {
37+
return "", "", "",
38+
fmt.Errorf("invalid element '%s'; route may only contain alphanumeric characters, '.', '_', and/or '-'", e)
39+
}
40+
if e == "." || e == ".." {
41+
return "", "", "", fmt.Errorf("invalid route element '%s'", e)
42+
}
43+
}
44+
45+
switch len(elements) {
46+
case 0:
47+
return "", "", "", fmt.Errorf("empty route")
48+
case 1:
49+
return "", "", "", fmt.Errorf("route has owner, but no repo")
50+
case 2:
51+
return elements[0], elements[1], "", nil
52+
case 3:
53+
if repoOnly {
54+
return "", "", "", fmt.Errorf("route is too deep")
55+
}
56+
return elements[0], elements[1], elements[2], nil
57+
default:
58+
return "", "", "", fmt.Errorf("route is too deep")
59+
}
60+
}

internal/core/funcs_test.go

+113
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,116 @@ func TestGetRouteFromUrl(t *testing.T) {
120120
})
121121
}
122122
}
123+
124+
var parseRouteTests = []struct {
125+
route string
126+
repoOnly bool
127+
expectedOwner string
128+
expectedRepo string
129+
expectedFile string
130+
expectedError bool
131+
}{
132+
// Valid routes
133+
{
134+
"test/repo/1.bundle",
135+
false,
136+
"test", "repo", "1.bundle",
137+
false,
138+
},
139+
{
140+
"test/repo",
141+
false,
142+
"test", "repo", "",
143+
false,
144+
},
145+
{
146+
"test_with_undercore/repo-with-dash",
147+
false,
148+
"test_with_undercore", "repo-with-dash", "",
149+
false,
150+
},
151+
{
152+
"//lots/of////path_separators...bundle//",
153+
false,
154+
"lots", "of", "path_separators...bundle",
155+
false,
156+
},
157+
{
158+
"test/repo",
159+
true,
160+
"test", "repo", "",
161+
false,
162+
},
163+
164+
// Invalid routes
165+
{
166+
"",
167+
false,
168+
"", "", "",
169+
true,
170+
},
171+
{
172+
"//",
173+
false,
174+
"", "", "",
175+
true,
176+
},
177+
{
178+
"too-short",
179+
false,
180+
"", "", "",
181+
true,
182+
},
183+
{
184+
"much/much/MUCH/too/long",
185+
false,
186+
"", "", "",
187+
true,
188+
},
189+
{
190+
"test/repo with spaces",
191+
false,
192+
"", "", "",
193+
true,
194+
},
195+
{
196+
"test/./repo",
197+
true,
198+
"", "", "",
199+
true,
200+
},
201+
{
202+
"../test/repo",
203+
true,
204+
"", "", "",
205+
true,
206+
},
207+
{
208+
"test/repo/1.bundle",
209+
true,
210+
"", "", "",
211+
true,
212+
},
213+
}
214+
215+
func TestParseRoute(t *testing.T) {
216+
for _, tt := range parseRouteTests {
217+
title := tt.route
218+
if tt.repoOnly {
219+
title += " (repo only)"
220+
}
221+
222+
t.Run(title, func(t *testing.T) {
223+
owner, repo, file, err := core.ParseRoute(tt.route, tt.repoOnly)
224+
225+
if tt.expectedError {
226+
assert.NotNil(t, err)
227+
} else {
228+
assert.Nil(t, err)
229+
assert.Equal(t, tt.expectedOwner, owner)
230+
assert.Equal(t, tt.expectedRepo, repo)
231+
assert.Equal(t, tt.expectedFile, file)
232+
}
233+
})
234+
}
235+
}

internal/core/repo.go

+12
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ func (r *repoProvider) CreateRepository(ctx context.Context, route string) (*Rep
5050
ctx, exitRegion := r.logger.Region(ctx, "repo", "create_repo")
5151
defer exitRegion()
5252

53+
ownerName, repoName, _, err := ParseRoute(route, true)
54+
if err != nil {
55+
return nil, err
56+
}
57+
route = ownerName + "/" + repoName
58+
5359
user, err := r.user.CurrentUser()
5460
if err != nil {
5561
return nil, err
@@ -93,6 +99,12 @@ func (r *repoProvider) RemoveRoute(ctx context.Context, route string) error {
9399
ctx, exitRegion := r.logger.Region(ctx, "repo", "remove_route")
94100
defer exitRegion()
95101

102+
ownerName, repoName, _, err := ParseRoute(route, true)
103+
if err != nil {
104+
return err
105+
}
106+
route = ownerName + "/" + repoName
107+
96108
repos, err := r.GetRepositories(ctx)
97109
if err != nil {
98110
return fmt.Errorf("failed to parse routes file: %w", err)

0 commit comments

Comments
 (0)