mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-23 21:04:55 +02:00
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:
1 parent
4b5f1f46de
commit
f270534e83
9 files changed
+257
-57
No files matched your search
@@ -72,6 +72,7 @@ type AccessTokenResponse struct {
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
func GeneralScopesSupported() []string {
|
||||
@@ -83,29 +84,53 @@ func GeneralScopesSupported() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// GrantAdditionalScopes returns valid scopes coming from grant
|
||||
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
|
||||
func splitGrantScopes(grantScopes string) (generalScopes []string, accessScope auth.AccessTokenScope) {
|
||||
generalScopesSupported := GeneralScopesSupported()
|
||||
var accessScopes []string // the scopes for access control, but not for general information
|
||||
var accessScopes []string
|
||||
hasAccessScopes := false
|
||||
for scope := range strings.SplitSeq(grantScopes, " ") {
|
||||
if scope != "" && !slices.Contains(generalScopesSupported, scope) {
|
||||
accessScopes = append(accessScopes, scope)
|
||||
if scope == "" {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(generalScopesSupported, scope) {
|
||||
generalScopes = append(generalScopes, scope)
|
||||
continue
|
||||
}
|
||||
hasAccessScopes = true
|
||||
for accessScope := range strings.SplitSeq(scope, ",") {
|
||||
if normalizedScope, err := auth.AccessTokenScope(accessScope).Normalize(); err == nil {
|
||||
accessScopes = append(accessScopes, normalizedScope.StringSlice()...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasAccessScopes {
|
||||
return generalScopes, ""
|
||||
}
|
||||
|
||||
// since version 1.22, access tokens grant full access to the API
|
||||
// with this access is reduced only if additional scopes are provided
|
||||
if len(accessScopes) > 0 {
|
||||
accessTokenScope := auth.AccessTokenScope(strings.Join(accessScopes, ","))
|
||||
if normalizedAccessTokenScope, err := accessTokenScope.Normalize(); err == nil {
|
||||
return normalizedAccessTokenScope
|
||||
}
|
||||
// TODO: if there are invalid access scopes (err != nil),
|
||||
// then it is treated as "all", maybe in the future we should make it stricter to return an error
|
||||
// at the moment, to avoid breaking 1.22 behavior, invalid tokens are also treated as "all"
|
||||
// Since version 1.22, if no access scope is provided, then "all" access is granted.
|
||||
//
|
||||
// OAuth also permits ignoring requested scopes under server policy: https://www.rfc-editor.org/rfc/rfc6749.html#section-3.3
|
||||
// If after the removal the invalid scopes, there are no valid scopes left, we return no-token-scope to prevent granting "all" access.
|
||||
accessScope, _ = auth.AccessTokenScope(strings.Join(accessScopes, ",")).Normalize()
|
||||
if accessScope == "" {
|
||||
accessScope = auth.AccessTokenScopeNone
|
||||
}
|
||||
// fallback, empty access scope is treated as "all" access
|
||||
return auth.AccessTokenScopeAll
|
||||
return generalScopes, accessScope
|
||||
}
|
||||
|
||||
// NormalizeGrantScope retains OIDC scopes and uses no-token-scope to prevent discarded scopes from granting full access.
|
||||
func NormalizeGrantScope(grantScopes string) string {
|
||||
generalScopes, accessScope := splitGrantScopes(grantScopes)
|
||||
return strings.Join(append(generalScopes, accessScope.StringSlice()...), " ")
|
||||
}
|
||||
|
||||
// GrantAdditionalScopes returns valid scopes coming from grant.
|
||||
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
|
||||
_, accessScope := splitGrantScopes(grantScopes)
|
||||
if accessScope == "" {
|
||||
return auth.AccessTokenScopeAll // grants without access scopes retain the legacy default
|
||||
}
|
||||
return accessScope
|
||||
}
|
||||
|
||||
func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt.NumericDate) jwt.RegisteredClaims {
|
||||
@@ -241,6 +266,7 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
|
||||
|
||||
return &AccessTokenResponse{
|
||||
AccessToken: signedAccessToken,
|
||||
Scope: NormalizeGrantScope(grant.Scope),
|
||||
TokenType: TokenTypeBearer,
|
||||
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
|
||||
RefreshToken: signedRefreshToken,
|
||||
|
||||
@@ -23,10 +23,15 @@ func TestGrantAdditionalScopes(t *testing.T) {
|
||||
{"read:user read:repository", "read:repository,read:user"},
|
||||
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
|
||||
{"openid profile email read:user", "read:user"},
|
||||
|
||||
// TODO: at the moment invalid tokens are treated as "all" to avoid breaking 1.22 behavior (more details are in GrantAdditionalScopes)
|
||||
{"read:invalid_scope", "all"},
|
||||
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
|
||||
{"no-token-scope", "no-token-scope"},
|
||||
{"openid no-token-scope", "no-token-scope"},
|
||||
{"no-token-scope read:user", "read:user"},
|
||||
{"read:invalid_scope", "no-token-scope"},
|
||||
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "no-token-scope"},
|
||||
{"openid invalid:scope", "no-token-scope"},
|
||||
{"openid ,,", "no-token-scope"},
|
||||
{"read:user invalid:scope", "read:user"},
|
||||
{"read:user,invalid:scope public-only", "public-only,read:user"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
Reference in new issue
Block a user