package service import ( "errors" "strings" "time" "github.com/golang-jwt/jwt/v5" "hy2xs-admin/dao" "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" ) // Токены подписываются и проверяются библиотекой golang-jwt/jwt/v5. // // Переход с v3 не косметический. У `github.com/golang-jwt/jwt` v3.2.2 есть // GO-2025-3553 (чрезмерное выделение памяти при разборе заголовка), и у неё // НЕТ исправленной версии в ветке v3: `Fixed in: N/A`. Уязвимый код // достигается из ParseToken, то есть с неаутентифицированного запроса — любой, // кто может обратиться к панели, попадает в него до всякой проверки прав. // Обновить здесь было нечего, поэтому лечится только сменой мажорной ветки. // // Заодно закрыт второй, тихий недостаток прежней реализации: keyfunc возвращал // ключ, НЕ проверяя алгоритм подписи. Разбор без ограничения набора алгоритмов // — это тот класс ошибок, из которого растёт подмена алгоритма; ниже стоит // jwt.WithValidMethods, и допустимый алгоритм ровно один. const TokenExpireDuration = time.Hour * 24 // tokenIssuer попадает и в подпись, и в проверку: токен, выписанный не нами, // не должен разбираться как наш даже при совпадении ключа. const tokenIssuer = "hy2xs-admin" type MyClaims struct { Admin bo.AccountBo `json:"admin"` jwt.RegisteredClaims } func jwtSecret() ([]byte, error) { config, err := dao.GetConfig("key = ?", constant.JwtSecret) if err != nil { return nil, errors.New(constant.SysError) } if config.Value == nil || strings.TrimSpace(*config.Value) == "" { // Подпись пустым ключом дала бы токены, которые подделывает кто угодно. // Раньше значение разыменовывалось без проверки: пустой JWT_SECRET // молча превращался в ключ нулевой длины. return nil, errors.New(constant.SysError) } return []byte(*config.Value), nil } func GenToken(accountBo bo.AccountBo) (string, error) { secret, err := jwtSecret() if err != nil { return "", err } now := time.Now() claims := MyClaims{ Admin: accountBo, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(TokenExpireDuration)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Issuer: tokenIssuer, }, } return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret) } func ParseToken(tokenString string) (*MyClaims, error) { secret, err := jwtSecret() if err != nil { return nil, err } token, err := jwt.ParseWithClaims( tokenString, &MyClaims{}, func(*jwt.Token) (interface{}, error) { return secret, nil }, // Допустимый алгоритм ровно один. Без этого ограничения набор // алгоритмов задаёт сам токен, то есть недоверенный вход. jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithIssuer(tokenIssuer), // Токен без срока жизни не должен считаться валидным бессрочным. jwt.WithExpirationRequired(), ) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, errors.New(constant.TokenExpiredError) } return nil, errors.New(constant.IllegalTokenError) } claims, ok := token.Claims.(*MyClaims) if !ok || !token.Valid { return nil, errors.New(constant.IllegalTokenError) } return claims, nil } // Функции GetToken здесь больше нет. // // Она доставала токен из заголовка Authorization вторым, независимым от // middleware.JWTHandler способом — и делала это через // strings.SplitN(tokenStr, " ", 2)[1], то есть паниковала на заголовке без // пробела: `Authorization: abc` давал index out of range. Единственный // потребитель — резервная ветка GetAdminInfo, которая была недостижима и // проверяла меньше, чем middleware. Разбор токена у продукта ровно один.