fix: 修复多个安全缺陷和功能问题
- 移除加密相关代码(服务端和所有客户端) - 修复 JWT 验证未检查签名算法的问题 - 修复重置密码使用不安全随机数的问题 - 实现 Logout 服务端失效机制 - 修复历史版本对比 diff 颜色标记错误 - 修复占位符解析循环引用风险 - 添加用户禁用状态检查 - 更新 bug 清单文档
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/middleware"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -51,5 +54,11 @@ func (h *AuthHandler) GetLoginUserInfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
userInfo := middleware.GetUserInfo(c)
|
||||
if userInfo != nil {
|
||||
userId, _ := strconv.Atoi(userInfo.Id)
|
||||
now := time.Now()
|
||||
dao.Engine.ID(userId).Cols("last_logout_at").Update(&model.User{LastLogoutAt: &now})
|
||||
}
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 0})
|
||||
}
|
||||
|
||||
@@ -13,13 +13,12 @@ import (
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
cryptoService *service.CryptoService
|
||||
parseService *service.ConfigParseService
|
||||
permService *service.PermissionService
|
||||
}
|
||||
|
||||
func NewConfigHandler(crypto *service.CryptoService, parse *service.ConfigParseService, perm *service.PermissionService) *ConfigHandler {
|
||||
return &ConfigHandler{cryptoService: crypto, parseService: parse, permService: perm}
|
||||
func NewConfigHandler(parse *service.ConfigParseService, perm *service.PermissionService) *ConfigHandler {
|
||||
return &ConfigHandler{parseService: parse, permService: perm}
|
||||
}
|
||||
|
||||
type ConfigListRequest struct {
|
||||
@@ -162,9 +161,6 @@ func (h *ConfigHandler) Add(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt sensitive values
|
||||
h.encryptIfRequired(&config)
|
||||
|
||||
if _, err := dao.Engine.Insert(&config); err != nil {
|
||||
c.JSON(http.StatusOK, model.WrapperResponse{Code: 500, Msg: "保存失败"})
|
||||
return
|
||||
@@ -208,8 +204,6 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
now := time.Now()
|
||||
config.UpdatedAt = &now
|
||||
|
||||
h.encryptIfRequired(&config)
|
||||
|
||||
dao.Engine.ID(config.Id).Cols("contents", "version", "updated_by", "updated_at").Update(&config)
|
||||
|
||||
// TODO: publish config change event (ZK removed, could use webhook or other mechanism)
|
||||
@@ -312,9 +306,3 @@ func (h *ConfigHandler) saveHistory(config *model.AppConfig, operator string) {
|
||||
}
|
||||
dao.Engine.Insert(&history)
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) encryptIfRequired(config *model.AppConfig) {
|
||||
// simplified: check if contents contain sensitive keys
|
||||
// In production, parse the config and encrypt values with {Cipher} prefix
|
||||
// For now, skip encryption logic
|
||||
}
|
||||
|
||||
@@ -13,12 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type ConfigApiHandler struct {
|
||||
cryptoService *service.CryptoService
|
||||
parseService *service.ConfigParseService
|
||||
}
|
||||
|
||||
func NewConfigApiHandler(crypto *service.CryptoService, parse *service.ConfigParseService) *ConfigApiHandler {
|
||||
return &ConfigApiHandler{cryptoService: crypto, parseService: parse}
|
||||
func NewConfigApiHandler(parse *service.ConfigParseService) *ConfigApiHandler {
|
||||
return &ConfigApiHandler{parseService: parse}
|
||||
}
|
||||
|
||||
type FetchAllRequest struct {
|
||||
@@ -104,11 +103,6 @@ func (h *ConfigApiHandler) FetchAllConfigs(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// add encryption keys
|
||||
globalCryptKey := h.cryptoService.GetCryptKey(0, req.Env)
|
||||
result["kenaito.config.global-encrypt-secret"] = globalCryptKey
|
||||
appCryptKey := h.cryptoService.GetCryptKey(app.Id, req.Env)
|
||||
result["kenaito.config.encrypt-secret"] = appCryptKey
|
||||
result["latest.fetch.timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"kenaito-conf/server/internal/model"
|
||||
"kenaito-conf/server/internal/service"
|
||||
"kenaito-conf/server/internal/util"
|
||||
"math/rand"
|
||||
"crypto/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -341,8 +341,9 @@ func (h *UserHandler) Options(c *gin.Context) {
|
||||
func randomString(n int) string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, n)
|
||||
_, _ = rand.Read(b)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
b[i] = letters[int(b[i])%len(letters)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -3,18 +3,19 @@ package model
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
GroupId *int `xorm:"int" json:"groupId"`
|
||||
Name string `xorm:"varchar(16)" json:"name"`
|
||||
Password string `xorm:"varchar(64)" json:"-"`
|
||||
Mobile string `xorm:"char(11)" json:"mobile"`
|
||||
Email string `xorm:"varchar(32)" json:"email"`
|
||||
Type string `xorm:"varchar(16) default 'user'" json:"type"` // user, groupAdmin, superAdmin
|
||||
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
|
||||
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
|
||||
UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"`
|
||||
GroupName string `xorm:"-" json:"groupName,omitempty"`
|
||||
GroupMaster bool `xorm:"-" json:"groupMaster,omitempty"`
|
||||
Id int `xorm:"int autoincr pk" json:"id"`
|
||||
GroupId *int `xorm:"int" json:"groupId"`
|
||||
Name string `xorm:"varchar(16)" json:"name"`
|
||||
Password string `xorm:"varchar(64)" json:"-"`
|
||||
Mobile string `xorm:"char(11)" json:"mobile"`
|
||||
Email string `xorm:"varchar(32)" json:"email"`
|
||||
Type string `xorm:"varchar(16) default 'user'" json:"type"` // user, groupAdmin, superAdmin
|
||||
Enabled bool `xorm:"tinyint(1) default 1" json:"enabled"`
|
||||
CreatedAt *time.Time `xorm:"datetime" json:"createdAt"`
|
||||
UpdatedAt *time.Time `xorm:"datetime" json:"updatedAt"`
|
||||
LastLogoutAt *time.Time `xorm:"datetime" json:"-"`
|
||||
GroupName string `xorm:"-" json:"groupName,omitempty"`
|
||||
GroupMaster bool `xorm:"-" json:"groupMaster,omitempty"`
|
||||
}
|
||||
|
||||
type UserPermission struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"kenaito-conf/server/internal/dao"
|
||||
"kenaito-conf/server/internal/model"
|
||||
"strconv"
|
||||
@@ -47,12 +48,14 @@ func (s *AuthService) Login(userName, password string) (string, *model.LoginUser
|
||||
s.loadPermissions(userInfo, user.Id)
|
||||
|
||||
// generate JWT
|
||||
now := time.Now()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"userId": user.Id,
|
||||
"name": user.Name,
|
||||
"type": user.Type,
|
||||
"groupId": user.GroupId,
|
||||
"exp": time.Now().Add(time.Duration(s.expireHrs) * time.Hour).Unix(),
|
||||
"iat": now.Unix(),
|
||||
"exp": now.Add(time.Duration(s.expireHrs) * time.Hour).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||
if err != nil {
|
||||
@@ -64,6 +67,9 @@ func (s *AuthService) Login(userName, password string) (string, *model.LoginUser
|
||||
|
||||
func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(s.jwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -82,6 +88,19 @@ func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, e
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
// 检查用户是否已被禁用
|
||||
if !user.Enabled {
|
||||
return nil, errors.New("user is disabled")
|
||||
}
|
||||
|
||||
// 检查 token 是否在登出后签发
|
||||
if user.LastLogoutAt != nil {
|
||||
iat := int64(claims["iat"].(float64))
|
||||
if iat <= user.LastLogoutAt.Unix() {
|
||||
return nil, errors.New("token is invalid after logout")
|
||||
}
|
||||
}
|
||||
|
||||
userInfo := &model.LoginUserInfo{
|
||||
Id: strconv.Itoa(user.Id),
|
||||
Name: user.Name,
|
||||
|
||||
@@ -133,6 +133,8 @@ func (s *ConfigParseService) resolvePlaceholders(result map[string]interface{})
|
||||
func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, value string) string {
|
||||
result := value
|
||||
maxIterations := 100
|
||||
processedPlaceholders := make(map[string]bool)
|
||||
|
||||
for i := 0; i < maxIterations; i++ {
|
||||
start := strings.Index(result, "${")
|
||||
if start < 0 {
|
||||
@@ -154,6 +156,12 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
|
||||
defaultVal = placeholder[idx+1:]
|
||||
}
|
||||
|
||||
// 检测循环引用
|
||||
if processedPlaceholders[refKey] {
|
||||
// 已经处理过这个占位符,说明存在循环引用,保留原样
|
||||
break
|
||||
}
|
||||
|
||||
var replacement string
|
||||
if v, ok := data[refKey]; ok {
|
||||
replacement = fmt.Sprintf("%v", v)
|
||||
@@ -163,6 +171,9 @@ func (s *ConfigParseService) replacePlaceholder(data map[string]interface{}, val
|
||||
replacement = "${" + placeholder + "}"
|
||||
}
|
||||
|
||||
// 标记已处理
|
||||
processedPlaceholders[refKey] = true
|
||||
|
||||
result = result[:start] + replacement + result[end+1:]
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"kenaito-conf/server/internal/config"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const CryptPrefix = "{Cipher}"
|
||||
|
||||
type CryptoService struct{}
|
||||
|
||||
func NewCryptoService() *CryptoService {
|
||||
return &CryptoService{}
|
||||
}
|
||||
|
||||
func (s *CryptoService) GetCryptKey(appId int, env string) string {
|
||||
base := config.Cfg.Crypto.SecretKey + fmt.Sprintf("%x", appId) + env
|
||||
h := md5.Sum([]byte(base))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func (s *CryptoService) Encrypt(appId int, env string, data string) string {
|
||||
key := s.GetCryptKey(appId, env)
|
||||
data = strings.ReplaceAll(data, CryptPrefix, "")
|
||||
encrypted := encryptWithAES(key, data)
|
||||
return CryptPrefix + encrypted
|
||||
}
|
||||
|
||||
func (s *CryptoService) IsEncrypted(appId int, env string, data string) bool {
|
||||
if !strings.HasPrefix(data, CryptPrefix) {
|
||||
return false
|
||||
}
|
||||
key := s.GetCryptKey(appId, env)
|
||||
_, err := decryptWithAES(key, strings.TrimPrefix(data, CryptPrefix))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *CryptoService) Decrypt(secret string, data string) (string, error) {
|
||||
return decryptWithAES(secret, data)
|
||||
}
|
||||
|
||||
func encryptWithAES(key string, data string) string {
|
||||
secretKey := md5.Sum([]byte(key))
|
||||
aesKey := hex.EncodeToString(secretKey[:])[16:]
|
||||
|
||||
block, err := aes.NewCipher([]byte(aesKey))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ECB mode (compatible with Java version)
|
||||
plainText := pkcs7Pad([]byte(data), aes.BlockSize)
|
||||
encrypted := make([]byte, len(plainText))
|
||||
mode := cipher.NewCBCEncrypter(block, make([]byte, aes.BlockSize))
|
||||
mode.CryptBlocks(encrypted, plainText)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(encrypted)
|
||||
}
|
||||
|
||||
func decryptWithAES(key string, data string) (string, error) {
|
||||
secretKey := md5.Sum([]byte(key))
|
||||
aesKey := hex.EncodeToString(secretKey[:])[16:]
|
||||
|
||||
cipherText, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher([]byte(aesKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(cipherText)%aes.BlockSize != 0 {
|
||||
return "", fmt.Errorf("ciphertext is not a multiple of block size")
|
||||
}
|
||||
|
||||
// ECB mode (compatible with Java version)
|
||||
plainText := make([]byte, len(cipherText))
|
||||
mode := cipher.NewCBCDecrypter(block, make([]byte, aes.BlockSize))
|
||||
mode.CryptBlocks(plainText, cipherText)
|
||||
|
||||
plainText, err = pkcs7Unpad(plainText, aes.BlockSize)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plainText), nil
|
||||
}
|
||||
|
||||
func pkcs7Pad(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padText := make([]byte, padding)
|
||||
for i := range padText {
|
||||
padText[i] = byte(padding)
|
||||
}
|
||||
return append(data, padText...)
|
||||
}
|
||||
|
||||
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("empty data")
|
||||
}
|
||||
padding := int(data[len(data)-1])
|
||||
if padding > blockSize || padding == 0 {
|
||||
return nil, fmt.Errorf("invalid padding")
|
||||
}
|
||||
return data[:len(data)-padding], nil
|
||||
}
|
||||
Reference in New Issue
Block a user