mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-04 20:25:16 +03:00
perf(auth): look up api tokens by their hash index (#6915)
Every bearer-authenticated request, including each node-sync call, loaded all enabled api_tokens rows and compared each stored SHA-256 hash in constant time. Tokens are stored hashed, so the database can compare hashes directly: a timing leak there reveals only a stored hash, which cannot authenticate. The lookup is now one indexed seek on idx_api_tokens_token, which AutoMigrate creates from the model tag on new and existing databases; scope and expiry are still checked afterwards.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package panel
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -256,36 +255,26 @@ func requireExpectedScope(expectedScope string) (string, error) {
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
// MatchToken returns the enabled, non-expired api_token row whose stored
|
||||
// SHA-256 hash matches the presented bearer value, or (nil,false). The loop
|
||||
// scans every enabled row with constant-time compares, then applies expiry and
|
||||
// scope checks to avoid treating corrupt values as admin.
|
||||
// MatchToken seeks the enabled row by the presented value's SHA-256 (comparing hashes leaks nothing
|
||||
// usable), then checks scope and expiry so a corrupt value is never treated as admin.
|
||||
func (s *ApiTokenService) MatchToken(presented string) (*model.ApiToken, bool) {
|
||||
if presented == "" {
|
||||
return nil, false
|
||||
}
|
||||
db := database.GetDB()
|
||||
var rows []*model.ApiToken
|
||||
if err := db.Model(model.ApiToken{}).Where("enabled = ?", true).Find(&rows).Error; err != nil {
|
||||
var row model.ApiToken
|
||||
if err := db.Model(model.ApiToken{}).
|
||||
Where("token = ? AND enabled = ?", crypto.HashTokenSHA256(presented), true).
|
||||
First(&row).Error; err != nil {
|
||||
return nil, false
|
||||
}
|
||||
presentedHash := []byte(crypto.HashTokenSHA256(presented))
|
||||
var matched *model.ApiToken
|
||||
for _, r := range rows {
|
||||
if subtle.ConstantTimeCompare([]byte(r.Token), presentedHash) == 1 {
|
||||
matched = r
|
||||
}
|
||||
}
|
||||
if matched == nil {
|
||||
if !model.IsKnownApiScope(row.Scope) {
|
||||
return nil, false
|
||||
}
|
||||
if !model.IsKnownApiScope(matched.Scope) {
|
||||
if row.ExpiresAt != 0 && nowMilli() >= row.ExpiresAt {
|
||||
return nil, false
|
||||
}
|
||||
if matched.ExpiresAt != 0 && nowMilli() >= matched.ExpiresAt {
|
||||
return nil, false
|
||||
}
|
||||
return matched, true
|
||||
return &row, true
|
||||
}
|
||||
|
||||
// Match is the legacy boolean form for callers that do not need scope.
|
||||
|
||||
Reference in New Issue
Block a user