fix: validate "scope" for OAuth2, "state" for OpenID (#39642)

1. Fixes: https://github.com/go-gitea/gitea/issues/39641
* The OAuth2 authorize/grant endpoint accepted and persisted arbitrary
scope strings without validation, unlike personal access token creation
which rejects unknown scopes. Normalize and validate form.Scope before
creating or updating a grant, ignore invalid scopes
* `// TODO: at the moment invalid tokens are treated as "all" to avoid
breaking 1.22 behavior` is fixed.
2. To avoid CSRF attack for OpenID endpoint, add a state check

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Shivansh Gargandwxiaoguang authored and GitHub committed 2026-10-12 20:57:41 +00:00
1 parent 4b5f1f46de
commit f270534e83
9 files changed
+257 -57

No files matched your search

+7 -8
View File
@@ -54,7 +54,8 @@ type AccessTokenScope string
// for all categories, write implies read
const (
AccessTokenScopeAll AccessTokenScope = "all"
AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
AccessTokenScopeNone AccessTokenScope = "no-token-scope" // no token permission scopes, so no API permissions
AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
AccessTokenScopeReadActivityPub AccessTokenScope = "read:activitypub"
AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub"
@@ -147,6 +148,7 @@ var allAccessTokenScopes = []AccessTokenScope{
// allAccessTokenScopeBits contains all access token scopes.
var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
AccessTokenScopeAll: accessTokenScopeAllBits,
AccessTokenScopeNone: 0,
AccessTokenScopePublicOnly: accessTokenScopePublicOnlyBits,
AccessTokenScopeReadActivityPub: accessTokenScopeReadActivityPubBits,
AccessTokenScopeWriteActivityPub: accessTokenScopeWriteActivityPubBits,
@@ -211,11 +213,6 @@ func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTok
return scopes
}
// ContainsCategory checks if a list of categories contains a specific category
func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool {
return slices.Contains(categories, category)
}
// GetScopeLevelFromAccessMode converts permission access mode to scope level
func GetScopeLevelFromAccessMode(mode perm.AccessMode) AccessTokenScopeLevel {
switch mode {
@@ -286,12 +283,14 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
if err != nil {
return "", err
}
if bitmap == 0 && slices.Contains(s.StringSlice(), string(AccessTokenScopeNone)) {
return AccessTokenScopeNone, nil
}
return bitmap.toScope(), nil
}
func (s AccessTokenScope) HasPermissionScope() bool {
return s != "" && s != AccessTokenScopePublicOnly
return s != "" && s != AccessTokenScopeNone && s != AccessTokenScopePublicOnly
}
// PublicOnly checks if this token scope is limited to public resources
+16
View File
@@ -18,8 +18,14 @@ type scopeTestNormalize struct {
func TestAccessTokenScope_Normalize(t *testing.T) {
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
assert.False(t, AccessTokenScopeNone.HasPermissionScope())
tests := []scopeTestNormalize{
{"", "", nil},
{"no-token-scope", "no-token-scope", nil},
{"no-token-scope,no-token-scope", "no-token-scope", nil},
{"no-token-scope,read:user", "read:user", nil},
{"no-token-scope,public-only", "public-only", nil},
{"no-token-scope,all", "all", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
{"all", "all", nil},
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
@@ -58,10 +64,20 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
{"all", "write:package", true, nil},
{"write:package", "all", false, nil},
{"public-only", "read:issue", false, nil},
{"no-token-scope", "all", false, nil},
{"no-token-scope", "no-token-scope", true, nil},
}
for _, scope := range GetAccessTokenCategories() {
tests = append(tests,
scopeTestHasScope{
AccessTokenScopeNone,
AccessTokenScope("read:" + scope), false, nil,
},
scopeTestHasScope{
AccessTokenScopeNone,
AccessTokenScope("write:" + scope), false, nil,
},
scopeTestHasScope{
AccessTokenScope("read:" + scope),
AccessTokenScope("read:" + scope), true, nil,