feat(core): sync framework security hardening and accessibility improvements

- add util.Go with panic recovery for background goroutines
- add util.EscapeLike and explicit ESCAPE clause for SQL LIKE queries
- add DummyCheckPassword and subtle.ConstantTimeCompare against timing attacks
- enforce session ID rotation upon login/oauth callback to prevent session fixation
- add sliding window login failure rate limiting and oauth state rate limiting
- fix redis client capture race in pubsub listeners and wait on stop channel
- adjust global --primary to oklch(51.1% 0.262 276.966) for WCAG AA contrast
- fix semantic heading levels and missing aria-labels across UI components
- document security, concurrency, and a11y standards in AGENTS.md
This commit is contained in:
ryan
2026-08-27 23:01:28 +08:00
parent b66cf3ae9c
commit ae3b792e16
63 changed files with 570 additions and 176 deletions
+29 -2
View File
@@ -5,6 +5,7 @@ package oauth
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
@@ -58,6 +59,10 @@ func GetLoginURL(c *gin.Context) {
userID := GetUserIDFromSession(session)
sessionHash := hashSessionToken(token)
if err := reserveOAuthStateSlot(ctx, sessionHash); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
state := uuid.NewString()
payloadValue, err := encodeOAuthStatePayload(oauthStatePayload{
@@ -70,7 +75,7 @@ func GetLoginURL(c *gin.Context) {
response.AbortInternal(c, err.Error())
return
}
if err := db.Redis.Set(c.Request.Context(), db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
if err := db.Redis.Set(ctx, db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
response.AbortInternal(c, err.Error())
return
}
@@ -98,6 +103,24 @@ func buildAuthorizeURL(ctx context.Context, source *model.AuthSource, state stri
return authConfig.AuthCodeURL(state), nil
}
func reserveOAuthStateSlot(ctx context.Context, sessionHash string) error {
if db.Redis == nil || sessionHash == "" {
return nil
}
key := db.PrefixedKey(fmt.Sprintf(oauthStateLimitKeyFormat, sessionHash))
n, err := db.Redis.Incr(ctx, key).Result()
if err != nil {
return err
}
if n == 1 {
_ = db.Redis.Expire(ctx, key, OAuthStateCacheKeyExpiration).Err()
}
if n > oauthStateLimitMax {
return errors.New(errOAuthStateRateLimited)
}
return nil
}
// Authorize 发起指定认证源授权
// @Summary 发起指定认证源授权
// @Description 根据指定认证源名称发起 OAuth 授权,支持 purpose 参数用于区分登录和账号绑定场景。认证源必须已启用。
@@ -147,6 +170,10 @@ func Authorize(c *gin.Context) {
}
sessionHash := hashSessionToken(token)
if err := reserveOAuthStateSlot(ctx, sessionHash); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
state := uuid.NewString()
payloadValue, err := encodeOAuthStatePayload(oauthStatePayload{
@@ -159,7 +186,7 @@ func Authorize(c *gin.Context) {
response.AbortInternal(c, err.Error())
return
}
if err := db.Redis.Set(c.Request.Context(), db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
if err := db.Redis.Set(ctx, db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
response.AbortInternal(c, err.Error())
return
}