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:
@@ -2,6 +2,7 @@ package panel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -276,3 +277,56 @@ func TestRecreateByNameKeepsOneToken(t *testing.T) {
|
||||
t.Fatalf("token rows = %d, want 1", count)
|
||||
}
|
||||
}
|
||||
|
||||
// maxRowsPerQuery reports the largest number of rows any single SELECT inside
|
||||
// fn returned, which is how a table scan shows up.
|
||||
func maxRowsPerQuery(t *testing.T, fn func()) int64 {
|
||||
t.Helper()
|
||||
db := database.GetDB()
|
||||
var max int64
|
||||
cb := "test:count_api_token_rows"
|
||||
if err := db.Callback().Query().After("gorm:query").Register(cb, func(tx *gorm.DB) {
|
||||
if tx.RowsAffected > max {
|
||||
max = tx.RowsAffected
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatalf("register query callback: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Callback().Query().Remove(cb); err != nil {
|
||||
t.Errorf("remove query callback: %v", err)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
return max
|
||||
}
|
||||
|
||||
// Every authenticated request looks its bearer token up. The lookup must read
|
||||
// the matching row, not every enabled token.
|
||||
func TestMatchTokenReadsOnlyTheMatchingTokenRow(t *testing.T) {
|
||||
setupAPITokenTestDB(t)
|
||||
svc := &ApiTokenService{}
|
||||
|
||||
tokens := make([]string, 0, 25)
|
||||
for i := range 25 {
|
||||
created, err := svc.Create(fmt.Sprintf("token-%d", i), model.ApiScopeAdmin, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("create token %d: %v", i, err)
|
||||
}
|
||||
tokens = append(tokens, created.Token)
|
||||
}
|
||||
|
||||
rows := maxRowsPerQuery(t, func() {
|
||||
matched, ok := svc.MatchToken(tokens[7])
|
||||
if !ok {
|
||||
t.Fatal("the created token must match")
|
||||
}
|
||||
if matched.Name != "token-7" {
|
||||
t.Fatalf("matched %q, want token-7", matched.Name)
|
||||
}
|
||||
})
|
||||
|
||||
if rows > 1 {
|
||||
t.Fatalf("the lookup read %d token rows; it must read only the matching one", rows)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user