mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-06 21:00:13 +00:00
Fix http auth header parsing (#34936)
Using `strings.EqualFold` is wrong in many cases.
This commit is contained in:
parent
8cbec63cc7
commit
d6d643fe86
47
modules/auth/httpauth/httpauth.go
Normal file
47
modules/auth/httpauth/httpauth.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package httpauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type BasicAuth struct {
|
||||
Username, Password string
|
||||
}
|
||||
|
||||
type BearerToken struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
type ParsedAuthorizationHeader struct {
|
||||
BasicAuth *BasicAuth
|
||||
BearerToken *BearerToken
|
||||
}
|
||||
|
||||
func ParseAuthorizationHeader(header string) (ret ParsedAuthorizationHeader, _ bool) {
|
||||
parts := strings.Fields(header)
|
||||
if len(parts) != 2 {
|
||||
return ret, false
|
||||
}
|
||||
if util.AsciiEqualFold(parts[0], "basic") {
|
||||
s, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return ret, false
|
||||
}
|
||||
u, p, ok := strings.Cut(string(s), ":")
|
||||
if !ok {
|
||||
return ret, false
|
||||
}
|
||||
ret.BasicAuth = &BasicAuth{Username: u, Password: p}
|
||||
return ret, true
|
||||
} else if util.AsciiEqualFold(parts[0], "token") || util.AsciiEqualFold(parts[0], "bearer") {
|
||||
ret.BearerToken = &BearerToken{Token: parts[1]}
|
||||
return ret, true
|
||||
}
|
||||
return ret, false
|
||||
}
|
43
modules/auth/httpauth/httpauth_test.go
Normal file
43
modules/auth/httpauth/httpauth_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package httpauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseAuthorizationHeader(t *testing.T) {
|
||||
type parsed = ParsedAuthorizationHeader
|
||||
type basic = BasicAuth
|
||||
type bearer = BearerToken
|
||||
cases := []struct {
|
||||
headerValue string
|
||||
expected parsed
|
||||
ok bool
|
||||
}{
|
||||
{"", parsed{}, false},
|
||||
{"?", parsed{}, false},
|
||||
{"foo", parsed{}, false},
|
||||
{"any value", parsed{}, false},
|
||||
|
||||
{"Basic ?", parsed{}, false},
|
||||
{"Basic " + base64.StdEncoding.EncodeToString([]byte("foo")), parsed{}, false},
|
||||
{"Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), parsed{BasicAuth: &basic{"foo", "bar"}}, true},
|
||||
{"basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), parsed{BasicAuth: &basic{"foo", "bar"}}, true},
|
||||
|
||||
{"token value", parsed{BearerToken: &bearer{"value"}}, true},
|
||||
{"Token value", parsed{BearerToken: &bearer{"value"}}, true},
|
||||
{"bearer value", parsed{BearerToken: &bearer{"value"}}, true},
|
||||
{"Bearer value", parsed{BearerToken: &bearer{"value"}}, true},
|
||||
{"Bearer wrong value", parsed{}, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
ret, ok := ParseAuthorizationHeader(c.headerValue)
|
||||
assert.Equal(t, c.ok, ok, "header %q", c.headerValue)
|
||||
assert.Equal(t, c.expected, ret, "header %q", c.headerValue)
|
||||
}
|
||||
}
|
@ -8,13 +8,10 @@ import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -36,19 +33,6 @@ func ShortSha(sha1 string) string {
|
||||
return util.TruncateRunes(sha1, 10)
|
||||
}
|
||||
|
||||
// BasicAuthDecode decode basic auth string
|
||||
func BasicAuthDecode(encoded string) (string, string, error) {
|
||||
s, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if username, password, ok := strings.Cut(string(s), ":"); ok {
|
||||
return username, password, nil
|
||||
}
|
||||
return "", "", errors.New("invalid basic authentication")
|
||||
}
|
||||
|
||||
// VerifyTimeLimitCode verify time limit code
|
||||
func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool {
|
||||
if len(code) <= 18 {
|
||||
|
@ -26,25 +26,6 @@ func TestShortSha(t *testing.T) {
|
||||
assert.Equal(t, "veryverylo", ShortSha("veryverylong"))
|
||||
}
|
||||
|
||||
func TestBasicAuthDecode(t *testing.T) {
|
||||
_, _, err := BasicAuthDecode("?")
|
||||
assert.Equal(t, "illegal base64 data at input byte 0", err.Error())
|
||||
|
||||
user, pass, err := BasicAuthDecode("Zm9vOmJhcg==")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", user)
|
||||
assert.Equal(t, "bar", pass)
|
||||
|
||||
_, _, err = BasicAuthDecode("aW52YWxpZA==")
|
||||
assert.Error(t, err)
|
||||
|
||||
_, _, err = BasicAuthDecode("invalid")
|
||||
assert.Error(t, err)
|
||||
|
||||
_, _, err = BasicAuthDecode("YWxpY2U=") // "alice", no colon
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVerifyTimeLimitCode(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.InstallLock, true)()
|
||||
initGeneralSecret := func(secret string) {
|
||||
|
@ -110,3 +110,24 @@ func SplitTrimSpace(input, sep string) []string {
|
||||
}
|
||||
return stringList
|
||||
}
|
||||
|
||||
func asciiLower(b byte) byte {
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
return b + ('a' - 'A')
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
|
||||
// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
|
||||
func AsciiEqualFold(s, t string) bool { //nolint:revive // PascalCase
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(s); i++ {
|
||||
if asciiLower(s[i]) != asciiLower(t[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -4,18 +4,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -108,9 +106,8 @@ func InfoOAuth(ctx *context.Context) {
|
||||
|
||||
var accessTokenScope auth.AccessTokenScope
|
||||
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
|
||||
auths := strings.Fields(auHead)
|
||||
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
|
||||
accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, auths[1])
|
||||
if parsed, ok := httpauth.ParseAuthorizationHeader(auHead); ok && parsed.BearerToken != nil {
|
||||
accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, parsed.BearerToken.Token)
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,18 +124,12 @@ func InfoOAuth(ctx *context.Context) {
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func parseBasicAuth(ctx *context.Context) (username, password string, err error) {
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") {
|
||||
return base.BasicAuthDecode(authData)
|
||||
}
|
||||
return "", "", errors.New("invalid basic authentication")
|
||||
}
|
||||
|
||||
// IntrospectOAuth introspects an oauth token
|
||||
func IntrospectOAuth(ctx *context.Context) {
|
||||
clientIDValid := false
|
||||
if clientID, clientSecret, err := parseBasicAuth(ctx); err == nil {
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
if parsed, ok := httpauth.ParseAuthorizationHeader(authHeader); ok && parsed.BasicAuth != nil {
|
||||
clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
|
||||
app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID)
|
||||
if err != nil && !auth.IsErrOauthClientIDInvalid(err) {
|
||||
// this is likely a database error; log it and respond without details
|
||||
@ -465,16 +456,16 @@ func AccessTokenOAuth(ctx *context.Context) {
|
||||
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
|
||||
// if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
|
||||
if form.ClientID == "" || form.ClientSecret == "" {
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") {
|
||||
clientID, clientSecret, err := base.BasicAuthDecode(authData)
|
||||
if err != nil {
|
||||
if authHeader := ctx.Req.Header.Get("Authorization"); authHeader != "" {
|
||||
parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
|
||||
if !ok || parsed.BasicAuth == nil {
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot parse basic auth header",
|
||||
})
|
||||
return
|
||||
}
|
||||
clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
|
||||
// validate that any fields present in the form match the Basic auth header
|
||||
if form.ClientID != "" && form.ClientID != clientID {
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
|
@ -7,12 +7,11 @@ package auth
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -54,17 +53,15 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
baHead := req.Header.Get("Authorization")
|
||||
if len(baHead) == 0 {
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
auths := strings.SplitN(baHead, " ", 2)
|
||||
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
|
||||
parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
|
||||
if !ok || parsed.BasicAuth == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
uname, passwd, _ := base.BasicAuthDecode(auths[1])
|
||||
uname, passwd := parsed.BasicAuth.Username, parsed.BasicAuth.Password
|
||||
|
||||
// Check if username or password is a token
|
||||
isUsernameToken := len(passwd) == 0 || passwd == "x-oauth-basic"
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -97,9 +98,9 @@ func parseToken(req *http.Request) (string, bool) {
|
||||
|
||||
// check header token
|
||||
if auHead := req.Header.Get("Authorization"); auHead != "" {
|
||||
auths := strings.Fields(auHead)
|
||||
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
|
||||
return auths[1], true
|
||||
parsed, ok := httpauth.ParseAuthorizationHeader(auHead)
|
||||
if ok && parsed.BearerToken != nil {
|
||||
return parsed.BearerToken.Token, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
lfs_module "code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -594,20 +595,12 @@ func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Rep
|
||||
if authorization == "" {
|
||||
return nil, errors.New("no token")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authorization, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("no token")
|
||||
}
|
||||
tokenSHA := parts[1]
|
||||
switch strings.ToLower(parts[0]) {
|
||||
case "bearer":
|
||||
fallthrough
|
||||
case "token":
|
||||
return handleLFSToken(ctx, tokenSHA, target, mode)
|
||||
}
|
||||
parsed, ok := httpauth.ParseAuthorizationHeader(authorization)
|
||||
if !ok || parsed.BearerToken == nil {
|
||||
return nil, errors.New("token not found")
|
||||
}
|
||||
return handleLFSToken(ctx, parsed.BearerToken.Token, target, mode)
|
||||
}
|
||||
|
||||
func requireAuth(ctx *context.Context) {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
|
||||
|
Loading…
Reference in New Issue
Block a user