Do not include secrets in the config hash

This commit is contained in:
binwiederhier
2026-07-23 08:13:27 +02:00
parent 7680cb4906
commit f2d5c1ce6c
3 changed files with 75 additions and 44 deletions
+12 -8
View File
@@ -130,9 +130,9 @@ type Config struct {
AuthFile string
AuthStartupQueries string
AuthDefault user.Permission
AuthUsers []*user.User
AuthUsers []*user.User `hash:"-"`
AuthAccess map[string][]*user.Grant
AuthTokens map[string][]*user.Token
AuthTokens map[string][]*user.Token `hash:"-"`
AuthBcryptCost int
AuthStatsQueueWriterInterval time.Duration
AuthAccessCacheEnabled bool // Enables the in-memory ACL cache (high volume servers only)
@@ -153,17 +153,17 @@ type Config struct {
FirebasePollInterval time.Duration
FirebaseQuotaExceededPenaltyDuration time.Duration
UpstreamBaseURL string
UpstreamAccessToken string
UpstreamAccessToken string `hash:"-"`
SMTPSenderAddr string
SMTPSenderUser string
SMTPSenderPass string
SMTPSenderPass string `hash:"-"`
SMTPSenderFrom string
SMTPSenderVerify bool
SMTPServerListen string
SMTPServerDomain string
SMTPServerAddrPrefix string
TwilioAccount string
TwilioAuthToken string
TwilioAuthToken string `hash:"-"`
TwilioPhoneNumber string
TwilioCallsBaseURL string
TwilioVerifyBaseURL string
@@ -198,8 +198,8 @@ type Config struct {
BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported)
ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported)
ProxyTrustedPrefixes []netip.Prefix // List of trusted proxy networks (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true
StripeSecretKey string
StripeWebhookKey string
StripeSecretKey string `hash:"-"`
StripeWebhookKey string `hash:"-"`
StripePriceCacheDuration time.Duration
BillingContact string
EnableSignup bool // Enable creation of accounts via API and UI
@@ -208,7 +208,7 @@ type Config struct {
EnableReservations bool // Allow users with role "user" to own/reserve topics
EnableMetrics bool
AccessControlAllowOrigin string // CORS header field to restrict access from web clients
WebPushPrivateKey string
WebPushPrivateKey string `hash:"-"`
WebPushPublicKey string
WebPushFile string
WebPushEmailAddress string
@@ -342,6 +342,10 @@ func (c *Config) Hash() string {
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldName := t.Field(i).Name
// Secrets must not feed the hash
if t.Field(i).Tag.Get("hash") == "-" {
continue
}
// Try to marshal the field and skip if it fails (e.g. *template.Template, netip.Prefix)
if b, err := json.Marshal(field.Interface()); err == nil {
result += fmt.Sprintf("%s:%s|", fieldName, string(b))
+22
View File
@@ -3,6 +3,7 @@ package server_test
import (
"github.com/stretchr/testify/assert"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"testing"
)
@@ -11,3 +12,24 @@ func TestConfig_New(t *testing.T) {
assert.Equal(t, ":80", c.ListenHTTP)
assert.Equal(t, server.DefaultKeepaliveInterval, c.KeepaliveInterval)
}
func TestConfig_HashExcludesSecrets(t *testing.T) {
// The config hash is served to browsers (ConfigHash, for webapp change detection), so
// secret material must not feed it: a weak secret would otherwise be offline-brute-forceable
// against a publicly visible hash.
conf1 := server.NewConfig()
conf2 := server.NewConfig()
conf2.StripeSecretKey = "sk_live_topsecret"
conf2.StripeWebhookKey = "whsec_topsecret"
conf2.TwilioAuthToken = "twilio-auth-token"
conf2.UpstreamAccessToken = "tk_upstream"
conf2.WebPushPrivateKey = "web-push-private-key"
conf2.SMTPSenderPass = "hunter2"
conf2.AuthUsers = []*user.User{{Name: "phil", Hash: "$2a$10$somebcrypthash"}}
conf2.AuthTokens = map[string][]*user.Token{"phil": {{Value: "tk_secrettoken"}}}
assert.Equal(t, conf1.Hash(), conf2.Hash())
// Non-secret fields must still change the hash
conf3 := server.NewConfig()
conf3.BaseURL = "https://ntfy.example.com"
assert.NotEqual(t, conf1.Hash(), conf3.Hash())
}