Skip to content

Commit 4251339

Browse files
authored
fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008)
An authenticated SSH user could pass a malformed sub-verb (e.g. `git-lfs-authenticate <repo> badverb`) so getAccessMode falls through to AccessModeNone (0). The permission check in routers/private/serv.go then evaluates `userMode < 0` which is always false, granting a valid LFS JWT for any private repository. The HTTP LFS handler only validates the Op claim on writes, so the token works for downloads. Validate the sub-verb in runServ before calling getAccessMode and fail fast for anything other than upload/download.
1 parent 743bbaa commit 4251339

2 files changed

Lines changed: 70 additions & 9 deletions

File tree

cmd/serv.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,25 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
113113
return nil
114114
}
115115

116-
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
116+
// getAccessMode maps an SSH git/LFS verb to the access mode it requires, with
117+
// ok=false for an unrecognised verb. Callers MUST reject the request when ok is
118+
// false: AccessModeNone would otherwise pass the `userMode < mode` permission
119+
// check in routers/private/serv.go and grant access.
120+
func getAccessMode(verb, lfsVerb string) (mode perm.AccessMode, ok bool) {
117121
switch verb {
118122
case git.CmdVerbUploadPack, git.CmdVerbUploadArchive:
119-
return perm.AccessModeRead
123+
return perm.AccessModeRead, true
120124
case git.CmdVerbReceivePack:
121-
return perm.AccessModeWrite
125+
return perm.AccessModeWrite, true
122126
case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer:
123127
switch lfsVerb {
124128
case git.CmdSubVerbLfsUpload:
125-
return perm.AccessModeWrite
129+
return perm.AccessModeWrite, true
126130
case git.CmdSubVerbLfsDownload:
127-
return perm.AccessModeRead
131+
return perm.AccessModeRead, true
128132
}
129133
}
130-
// should be unreachable
131-
setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb)
132-
return perm.AccessModeNone
134+
return perm.AccessModeNone, false
133135
}
134136

135137
func runServ(ctx context.Context, c *cli.Command) error {
@@ -247,7 +249,10 @@ func runServ(ctx context.Context, c *cli.Command) error {
247249
}
248250
}
249251

250-
requestedMode := getAccessMode(verb, lfsVerb)
252+
requestedMode, ok := getAccessMode(verb, lfsVerb)
253+
if !ok {
254+
return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb)
255+
}
251256

252257
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
253258
if extra.HasError() {

cmd/serv_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package cmd
5+
6+
import (
7+
"testing"
8+
9+
"gitea.dev/models/perm"
10+
"gitea.dev/modules/git"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestGetAccessMode(t *testing.T) {
16+
cases := []struct {
17+
verb, lfsVerb string
18+
expected perm.AccessMode
19+
}{
20+
{git.CmdVerbUploadPack, "", perm.AccessModeRead},
21+
{git.CmdVerbUploadArchive, "", perm.AccessModeRead},
22+
{git.CmdVerbReceivePack, "", perm.AccessModeWrite},
23+
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
24+
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
25+
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
26+
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
27+
}
28+
for _, tc := range cases {
29+
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
30+
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
31+
assert.True(t, ok)
32+
assert.Equal(t, tc.expected, mode)
33+
})
34+
}
35+
}
36+
37+
// TestGetAccessModeUnknownVerb locks in the invariant that getAccessMode reports
38+
// ok=false for unrecognised verbs and LFS sub-verbs, so runServ rejects them. An
39+
// unknown verb has no valid access mode; if it were treated as AccessModeNone (0)
40+
// it would pass the `userMode < mode` permission check in routers/private/serv.go
41+
// and hand out valid LFS JWTs for any private repository.
42+
func TestGetAccessModeUnknownVerb(t *testing.T) {
43+
cases := []struct{ verb, lfsVerb string }{
44+
{git.CmdVerbLfsAuthenticate, ""},
45+
{git.CmdVerbLfsAuthenticate, "badverb"},
46+
{git.CmdVerbLfsTransfer, "badverb"},
47+
{"git-unknown-verb", ""},
48+
}
49+
for _, tc := range cases {
50+
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
51+
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
52+
assert.False(t, ok)
53+
assert.Equal(t, perm.AccessModeNone, mode)
54+
})
55+
}
56+
}

0 commit comments

Comments
 (0)