CVE-2024-56362 (Go)

Navidrome is an open source web-based music collection server and streamer. Navidrome stores the JWT secret in plaintext in the navidrome.db database file under the property table. This practice introduces a security risk because anyone with access to the database file can retrieve the secret. This vulnerability is fixed in 0.54.1.

Weakness:
CWE-312: name: Cleartext Storage of Sensitive Information
description: The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.

Repository: https://github.com/navidrome/navidrome

Vulnerable code:
file_path: core/auth/auth.go
snippet: func Init(ds model.DataStore) {
	once.Do(func() {
		log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)
		secret, err := ds.Property(context.TODO()).Get(consts.JWTSecretKey)
		if err != nil || secret == "" {
			log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
			secret = uuid.NewString()
		}
		Secret = []byte(secret)
		TokenAuth = jwtauth.New("HS256", Secret, nil)
	})
}

Fixed code:
file_path: core/auth/auth.go
snippet: func Init(ds model.DataStore) {
	once.Do(func() {
		ctx := context.TODO()
		log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout)

		secret, err := ds.Property(ctx).Get(consts.JWTSecretKey)
		if err != nil || secret == "" {
			secret = createNewSecret(ctx, ds)
		} else {
			if secret, err = utils.Decrypt(ctx, getEncKey(), secret); err != nil {
				log.Error(ctx, "Could not decrypt JWT secret, creating a new one", err)
				secret = createNewSecret(ctx, ds)
			}
		}

		TokenAuth = jwtauth.New("HS256", []byte(secret), nil)
	})
}

file_path: core/auth/auth.go
snippet: var (
	once      sync.Once
	TokenAuth *jwtauth.JWTAuth
)

file_path: utils/encrypt.go
snippet: func Decrypt(ctx context.Context, encKey []byte, encData string) (value string, err error) {
	// Recover from any panics
	defer func() {
		if r := recover(); r != nil {
			log.Error(ctx, "Panic during decryption", r)
			err = errors.New("decryption panicked")
		}
	}()

	enc, _ := base64.StdEncoding.DecodeString(encData)

	block, err := aes.NewCipher(encKey)
	if err != nil {
		log.Error(ctx, "Could not create a cipher", err)
		return "", err
	}

	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		log.Error(ctx, "Could not create a GCM", err)
		return "", err
	}

	nonceSize := aesGCM.NonceSize()
	nonce, ciphertext := enc[:nonceSize], enc[nonceSize:]

	plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		log.Error(ctx, "Could not decrypt password", err)
		return "", err
	}

	return string(plaintext), nil

file_path: core/auth/auth.go
snippet: func createNewSecret(ctx context.Context, ds model.DataStore) string {
	log.Info(ctx, "Creating new JWT secret, used for encrypting UI sessions")
	secret := uuid.NewString()
	encSecret, err := utils.Encrypt(ctx, getEncKey(), secret)
	if err != nil {
		log.Error(ctx, "Could not encrypt JWT secret", err)
	}

	if err := ds.Property(ctx).Put(consts.JWTSecretKey, encSecret); err != nil {
		log.Error(ctx, "Could not save JWT secret in DB", err)
	}

	return secret
}

file_path: core/auth/auth.go
snippet: func getEncKey() []byte {
	key := cmp.Or(
		conf.Server.PasswordEncryptionKey,
		consts.DefaultEncryptionKey,
	)
	sum := sha256.Sum256([]byte(key))
	return sum[:]
}