dc351aa0d824c8bef528c6924a7acdfb9f20c852
- Author
- Kieran Klukas <kieran@dunkirk.sh>
- Committer
- Kieran Klukas <kieran@dunkirk.sh>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/pkg/web/auth.go b/pkg/web/auth.go
2index 86e15f1ef1a818b442756f80d2e48f0984f18e54..48a8e4e1fa352160a840bc424f3bd6a40b32f999 100644
3--- a/pkg/web/auth.go
4+++ b/pkg/web/auth.go
5@@ -141,9 +141,11 @@ func parseJWT(ctx context.Context, bearer string) (*jwt.RegisteredClaims, error)
6 return nil, err
7 }
8
9+ // No token can be valid for a missing repository. Reject it like any
10+ // other bad token so the answer does not reveal whether it exists.
11 repo := proto.RepositoryFromContext(ctx)
12 if repo == nil {
13- return nil, errors.New("missing repository")
14+ return nil, ErrInvalidToken
15 }
16
17 token, err := jwt.ParseWithClaims(bearer, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
18diff --git a/pkg/web/git.go b/pkg/web/git.go
19index 5807fefa993aac2f7edd6b1f83044dbec88f86cc..5083d83c02d61abea32aeb2cdfc3d1e1460468c9 100644
20--- a/pkg/web/git.go
21+++ b/pkg/web/git.go
22@@ -230,6 +230,41 @@ func badCredentials(err error) bool {
23 return errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrInvalidPassword)
24 }
25
26+// denyRead answers a caller who may not read the repository. Missing and
27+// unreadable repositories get the same answer so neither is revealed.
28+func denyRead(w http.ResponseWriter, r *http.Request, err error, lfsJSON bool) {
29+ switch {
30+ case badCredentials(err) && lfsJSON:
31+ renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{Message: "bad credentials"})
32+ case badCredentials(err):
33+ renderForbidden(w, r)
34+ case proto.UserFromContext(r.Context()) == nil && lfsJSON:
35+ askCredentials(w, r)
36+ renderJSON(w, http.StatusUnauthorized, lfs.ErrorResponse{Message: "credentials needed"})
37+ case proto.UserFromContext(r.Context()) == nil:
38+ askCredentials(w, r)
39+ renderUnauthorized(w, r)
40+ case lfsJSON:
41+ renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{Message: "repository not found"})
42+ default:
43+ renderNotFound(w, r)
44+ }
45+}
46+
47+// lfsNeedsWrite reports whether an LFS request needs write access.
48+// https://github.com/git-lfs/git-lfs/blob/main/docs/api/locking.md
49+func lfsNeedsWrite(file, method string) bool {
50+ switch {
51+ case strings.HasPrefix(file, "info/lfs/locks"):
52+ return strings.HasSuffix(file, "lfs/locks") ||
53+ strings.HasSuffix(file, "lfs/locks/verify") ||
54+ strings.HasSuffix(file, "/unlock") && method == http.MethodPost
55+ case strings.HasPrefix(file, "info/lfs/objects/basic"):
56+ return method == http.MethodPut
57+ }
58+ return false
59+}
60+
61 // requireCaller rejects anonymous requests when keyless access is disabled.
62 func requireCaller(w http.ResponseWriter, r *http.Request) bool {
63 ctx := r.Context()
64@@ -287,18 +322,10 @@ func withAccess(next http.Handler) http.HandlerFunc {
65
66 file := mux.Vars(r)["file"]
67
68- // We only allow these services to proceed any other services should return 403
69- // - git-upload-pack
70- // - git-receive-pack
71- // - git-lfs
72- //
73 // The LFS case must stay ahead of the service cases. "file" is derived
74- // from the request path by withParams, but "service" can come from a
75- // query parameter the caller controls, and withParams only fills it in
76- // for paths ending in git-upload-pack or git-receive-pack, so it is
77- // always caller-supplied on an LFS route. Matching the path first means
78- // an LFS request is authorized as LFS no matter what service it claims
79- // to be.
80+ // from the request path, but "service" can come from a query parameter
81+ // the caller controls, so an LFS request is authorized as LFS no
82+ // matter what service it claims to be.
83 switch {
84 case strings.HasPrefix(file, "info/lfs"):
85 if !cfg.LFS.Enabled {
86@@ -307,54 +334,15 @@ func withAccess(next http.Handler) http.HandlerFunc {
87 return
88 }
89
90- switch {
91- case strings.HasPrefix(file, "info/lfs/locks"):
92- switch {
93- case strings.HasSuffix(file, "lfs/locks"), strings.HasSuffix(file, "/unlock") && r.Method == http.MethodPost:
94- // Create lock, list locks, and delete lock require write access
95- fallthrough
96- case strings.HasSuffix(file, "lfs/locks/verify"):
97- // Locks verify requires write access
98- // https://github.com/git-lfs/git-lfs/blob/main/docs/api/locking.md#unauthorized-response-2
99- if accessLevel < access.ReadWriteAccess {
100- renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{
101- Message: "write access required",
102- })
103- return
104- }
105- }
106- case strings.HasPrefix(file, "info/lfs/objects/basic"):
107- switch r.Method {
108- case http.MethodPut:
109- // Basic upload
110- if accessLevel < access.ReadWriteAccess {
111- renderJSON(w, http.StatusForbidden, lfs.ErrorResponse{
112- Message: "write access required",
113- })
114- return
115- }
116- case http.MethodGet:
117- // Basic download
118- case http.MethodPost:
119- // Basic verify
120- }
121+ if repo == nil || accessLevel < access.ReadOnlyAccess {
122diff --git a/testscript/testdata/http.txtar b/testscript/testdata/http.txtar
123index 9dfe6a4d941192af201f2fbc494e4865f3deb81b..41bb207f6561e80399f9260481aa5073a057a783 100644
124--- a/testscript/testdata/http.txtar
125+++ b/testscript/testdata/http.txtar
126@@ -105,13 +105,54 @@ cmp stdout http3.txt
127 curl http://localhost:$HTTP_PORT/repo2.git/info/lfs/objects/batch
128 stdout '.*credentials needed.*'
129 curl http://$UTOKEN@localhost:$HTTP_PORT/repo2.git/info/lfs/objects/batch
130-stdout '.*credentials needed.*'
131+stdout '.*repository not found.*'
132 curl http://0$UTOKEN@localhost:$HTTP_PORT/repo2.git/info/lfs/objects/batch
133 cmp stdout http3.txt
134
135 # deny dumb http git
136 curl -XGET http://localhost:$HTTP_PORT/repo2.git/info/refs
137+stdout '401.*'
138+curl -XGET http://$UTOKEN@localhost:$HTTP_PORT/repo2.git/info/refs
139+stdout '404.*'
140+
141+# private and missing repos get the same answer
142+curl http://localhost:$HTTP_PORT/repo2.git/info/refs?service=git-upload-pack
143+stdout '401.*'
144+cp stdout private.txt
145+curl http://localhost:$HTTP_PORT/nope.git/info/refs?service=git-upload-pack
146+cmp stdout private.txt
147+curl http://$UTOKEN@localhost:$HTTP_PORT/repo2.git/info/refs?service=git-upload-pack
148 stdout '404.*'
149+cp stdout private.txt
150+curl http://$UTOKEN@localhost:$HTTP_PORT/nope.git/info/refs?service=git-upload-pack
151+cmp stdout private.txt
152+curl http://someuser:somepassword@localhost:$HTTP_PORT/repo2.git/info/refs?service=git-upload-pack
153+stdout '403.*'
154+cp stdout private.txt
155+curl http://someuser:somepassword@localhost:$HTTP_PORT/nope.git/info/refs?service=git-upload-pack
156+cmp stdout private.txt
157+curl -XPOST -H 'Accept: application/vnd.git-lfs+json' -H 'Content-Type: application/vnd.git-lfs+json' http://localhost:$HTTP_PORT/repo2.git/info/lfs/objects/batch
158+cp stdout private.txt
159+curl -XPOST -H 'Accept: application/vnd.git-lfs+json' -H 'Content-Type: application/vnd.git-lfs+json' http://localhost:$HTTP_PORT/nope.git/info/lfs/objects/batch
160+cmp stdout private.txt
161+curl -H 'Authorization: Bearer garbage' http://localhost:$HTTP_PORT/repo2.git/info/refs?service=git-upload-pack
162+stdout '403.*'
163+cp stdout private.txt
164+curl -H 'Authorization: Bearer garbage' http://localhost:$HTTP_PORT/nope.git/info/refs?service=git-upload-pack
165+cmp stdout private.txt
166+curl -H 'Authorization: Bearer garbage' http://localhost:$HTTP_PORT/repo2.git/HEAD
167+cp stdout private.txt
168+curl -H 'Authorization: Bearer garbage' http://localhost:$HTTP_PORT/nope.git/HEAD
169+cmp stdout private.txt
170+curl -XPOST -H 'Authorization: Bearer garbage' http://localhost:$HTTP_PORT/repo2.git/info/lfs/locks
171+cp stdout private.txt
172+curl -XPOST -H 'Authorization: Bearer garbage' http://localhost:$HTTP_PORT/nope.git/info/lfs/locks
173+cmp stdout private.txt
174+curl -XPOST http://$UTOKEN@localhost:$HTTP_PORT/repo2.git/info/lfs/locks
175+stdout '.*repository not found.*'
176+cp stdout private.txt
177+curl -XPOST http://$UTOKEN@localhost:$HTTP_PORT/nope.git/info/lfs/locks
178+cmp stdout private.txt
179
180 # deny access ask for credentials
181 # this means the server responded with a 401 and prompted for credentials
182@@ -157,9 +198,9 @@ stdout '404.*'
183
184 # go-get does not bypass repository checks on other routes
185 curl http://localhost:$HTTP_PORT/nope.git/info/lfs/objects/basic/0000000000000000000000000000000000000000000000000000000000000000?go-get=1
186-stdout '404.*'
187+stdout '.*credentials needed.*'
188 curl http://localhost:$HTTP_PORT/nope.git/objects/info/packs?go-get=1
189-stdout '404.*'
190+stdout '401.*'
191
192 # stop the server
193 [windows] stopserver