mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-28 15:51:29 +08:00
fix: use configured JWT secret in v1
Fixes CVE-2025-7453 / GHSA-2hfh-94w5-wxvf by replacing the v1 hardcoded JWT secret with a generated per-installation config secret.
This commit is contained in:
+11
-1
@@ -25,8 +25,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/saltbo/zpan/internal/app/service"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -73,5 +73,15 @@ func initConfig() {
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||
viper.Set("installed", true)
|
||||
if viper.GetString("security.jwt_secret") == "" {
|
||||
if err := service.EnsureJWTSecret(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -29,4 +29,14 @@ database:
|
||||
| sqlite3 | zpan.db |
|
||||
| mysql | user:pass@tcp(127.0.0.1:3306)/zpan?charset=utf8mb4&parseTime=True&loc=Local |
|
||||
| postgres | user=zpan password=zpan dbname=zpan port=9920 sslmode=disable TimeZone=Asia/Shanghai |
|
||||
| mssql | sqlserver://zpan:LoremIpsum86@localhost:9930?database=zpan |
|
||||
| mssql | sqlserver://zpan:LoremIpsum86@localhost:9930?database=zpan |
|
||||
|
||||
## security
|
||||
这里定义了ZPan的安全配置
|
||||
```yaml
|
||||
security:
|
||||
jwt_secret: your-random-secret
|
||||
```
|
||||
|
||||
### jwt_secret
|
||||
用于签发和校验登录令牌。安装和升级时会自动生成随机值,请不要与其他实例共用。
|
||||
|
||||
+11
-1
@@ -79,6 +79,16 @@ The dsn corresponding to different drivers is also different, here we give the d
|
||||
| postgres | user=zpan password=zpan dbname=zpan port=9920 sslmode=disable TimeZone=Asia/Shanghai |
|
||||
| mssql | sqlserver://zpan:LoremIpsum86@localhost:9930?database=zpan |
|
||||
|
||||
# security
|
||||
Configure security settings
|
||||
```yaml
|
||||
security:
|
||||
jwt_secret: your-random-secret
|
||||
```
|
||||
|
||||
### jwt_secret
|
||||
Used to sign and verify login tokens. ZPan generates a random value during installation and upgrade. Do not share it across instances.
|
||||
|
||||
# provider
|
||||
Currently we support all S3-based cloud storage platforms, such as Alibaba Cloud OSS, Tencent Cloud COS, Qiniu Cloud KODO.
|
||||
```yaml
|
||||
@@ -131,4 +141,4 @@ Mailing address,eg:no-reply@saltbo.fun
|
||||
Sender,eg:Zpan
|
||||
|
||||
### password
|
||||
Sending password
|
||||
Sending password
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/saltbo/gopkg/ginutil"
|
||||
"github.com/saltbo/gopkg/jwtutil"
|
||||
"github.com/saltbo/gopkg/strutil"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/saltbo/zpan/internal/app/dao"
|
||||
"github.com/saltbo/zpan/internal/app/model"
|
||||
"github.com/saltbo/zpan/internal/app/service"
|
||||
"github.com/saltbo/zpan/internal/pkg/authed"
|
||||
"github.com/saltbo/zpan/internal/pkg/bind"
|
||||
)
|
||||
@@ -21,8 +21,6 @@ import (
|
||||
const ShareCookieTokenKey = "share-token"
|
||||
|
||||
type ShareResource struct {
|
||||
jwtutil.JWTUtil
|
||||
|
||||
dShare *dao.Share
|
||||
dMatter *dao.Matter
|
||||
}
|
||||
@@ -170,7 +168,7 @@ func (rs *ShareResource) draw(c *gin.Context) {
|
||||
NotBefore: time.Now().Unix(),
|
||||
Subject: share.Alias,
|
||||
}
|
||||
token, err := rs.JWTUtil.Issue(claims)
|
||||
token, err := service.NewJWTUtil().Issue(claims)
|
||||
if err != nil {
|
||||
ginutil.JSONServerError(c, err)
|
||||
return
|
||||
@@ -250,7 +248,7 @@ func (rs *ShareResource) shareTokenVerify(c *gin.Context, share *model.Share) er
|
||||
return err
|
||||
}
|
||||
|
||||
if token, err := rs.JWTUtil.Parse(tokenStr, &jwt.StandardClaims{}); err != nil {
|
||||
if token, err := service.NewJWTUtil().Parse(tokenStr, &jwt.StandardClaims{}); err != nil {
|
||||
return err
|
||||
} else if token.Claims.(*jwt.StandardClaims).Subject != share.Alias {
|
||||
return fmt.Errorf("unmatched token")
|
||||
|
||||
@@ -3,15 +3,13 @@ package api
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/saltbo/gopkg/ginutil"
|
||||
"github.com/saltbo/gopkg/jwtutil"
|
||||
|
||||
"github.com/saltbo/zpan/internal/app/dao"
|
||||
"github.com/saltbo/zpan/internal/pkg/bind"
|
||||
"github.com/saltbo/zpan/internal/app/service"
|
||||
"github.com/saltbo/zpan/internal/pkg/bind"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
jwtutil.JWTUtil
|
||||
}
|
||||
|
||||
func NewStorageResource() *Storage {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/saltbo/gopkg/ginutil"
|
||||
"github.com/saltbo/gopkg/jwtutil"
|
||||
"github.com/saltbo/gopkg/strutil"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
@@ -18,8 +17,6 @@ import (
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
jwtutil.JWTUtil
|
||||
|
||||
sOption *service.Option
|
||||
}
|
||||
|
||||
@@ -63,6 +60,11 @@ func (rs *Option) setupDatabase(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.EnsureJWTSecret(); err != nil {
|
||||
ginutil.JSONServerError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
viper.Set("database.driver", p["driver"])
|
||||
viper.Set("database.dsn", p["dsn"])
|
||||
cfgFile := viper.ConfigFileUsed()
|
||||
|
||||
@@ -1,22 +1,76 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/saltbo/gopkg/jwtutil"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const jwtSecretConfigKey = "security.jwt_secret"
|
||||
|
||||
var temporaryJWTSecret string
|
||||
|
||||
type Token struct {
|
||||
}
|
||||
|
||||
func NewToken() *Token {
|
||||
jwtutil.Init("123")
|
||||
jwtutil.Init(JWTSecret())
|
||||
return &Token{}
|
||||
}
|
||||
|
||||
func NewJWTUtil() *jwtutil.JWTUtil {
|
||||
return jwtutil.New(JWTSecret())
|
||||
}
|
||||
|
||||
func EnsureJWTSecret() error {
|
||||
if viper.GetString(jwtSecretConfigKey) != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
secret, err := generateJWTSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viper.Set(jwtSecretConfigKey, secret)
|
||||
return nil
|
||||
}
|
||||
|
||||
func JWTSecret() string {
|
||||
if secret := viper.GetString(jwtSecretConfigKey); secret != "" {
|
||||
return secret
|
||||
}
|
||||
|
||||
if !viper.IsSet("installed") {
|
||||
if temporaryJWTSecret == "" {
|
||||
secret, err := generateJWTSecret()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("generate temporary jwt secret failed: %s", err))
|
||||
}
|
||||
temporaryJWTSecret = secret
|
||||
}
|
||||
|
||||
return temporaryJWTSecret
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("missing required config %q", jwtSecretConfigKey))
|
||||
}
|
||||
|
||||
func generateJWTSecret() (string, error) {
|
||||
buf := make([]byte, 32)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func (s *Token) Create(uid string, ttl int, roles ...string) (string, error) {
|
||||
return jwtutil.Issue(NewRoleClaims(uid, ttl, roles))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func resetTokenConfig(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
viper.Reset()
|
||||
temporaryJWTSecret = ""
|
||||
t.Cleanup(func() {
|
||||
viper.Reset()
|
||||
temporaryJWTSecret = ""
|
||||
})
|
||||
}
|
||||
|
||||
func TestTokenRejectsHardcodedSecret(t *testing.T) {
|
||||
resetTokenConfig(t)
|
||||
viper.Set("installed", true)
|
||||
viper.Set(jwtSecretConfigKey, "configured-secret")
|
||||
|
||||
tokenService := NewToken()
|
||||
forgedToken, err := jwt.NewWithClaims(
|
||||
jwt.SigningMethodHS512,
|
||||
NewRoleClaims("1", 3600, []string{"admin"}),
|
||||
).SignedString([]byte("123"))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tokenService.Verify(forgedToken)
|
||||
assert.Error(t, err)
|
||||
|
||||
validToken, err := tokenService.Create("1", 3600, "admin")
|
||||
require.NoError(t, err)
|
||||
|
||||
claims, err := tokenService.Verify(validToken)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "1", claims.Subject)
|
||||
assert.Equal(t, []string{"admin"}, claims.Roles)
|
||||
}
|
||||
|
||||
func TestTokenRequiresConfiguredSecretAfterInstall(t *testing.T) {
|
||||
resetTokenConfig(t)
|
||||
viper.Set("installed", true)
|
||||
|
||||
assert.Panics(t, func() {
|
||||
NewToken()
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnsureJWTSecretGeneratesPersistentConfig(t *testing.T) {
|
||||
resetTokenConfig(t)
|
||||
|
||||
require.NoError(t, EnsureJWTSecret())
|
||||
|
||||
secret := viper.GetString(jwtSecretConfigKey)
|
||||
assert.NotEmpty(t, secret)
|
||||
assert.NotEqual(t, "123", secret)
|
||||
}
|
||||
Reference in New Issue
Block a user