diff --git a/handlers/chip/handler.go b/handlers/chip/handler.go index 4aee81572..b17bcd2cf 100644 --- a/handlers/chip/handler.go +++ b/handlers/chip/handler.go @@ -8,6 +8,7 @@ import ( "github.com/nyaruka/courier" "github.com/nyaruka/courier/handlers" + "github.com/nyaruka/courier/utils" "github.com/nyaruka/gocommon/jsonx" "github.com/nyaruka/gocommon/urns" ) @@ -59,7 +60,7 @@ type receivePayload struct { // receiveMessage is our HTTP handler function for incoming events func (h *handler) receive(ctx context.Context, c courier.Channel, w http.ResponseWriter, r *http.Request, payload *receivePayload, clog *courier.ChannelLog) ([]courier.Event, error) { secret := c.StringConfigForKey(courier.ConfigSecret, "") - if payload.Secret != secret { + if !utils.SecretEqual(payload.Secret, secret) { return nil, handlers.WriteAndLogRequestError(ctx, h, c, w, r, errors.New("secret incorrect")) } diff --git a/server.go b/server.go index a02c527c1..a863e0bea 100644 --- a/server.go +++ b/server.go @@ -18,6 +18,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/nyaruka/courier/utils" "github.com/nyaruka/courier/utils/clogs" "github.com/nyaruka/gocommon/httpx" "github.com/nyaruka/gocommon/jsonx" @@ -393,7 +394,8 @@ func (s *server) basicAuthRequired(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if s.config.StatusUsername != "" { user, pass, ok := r.BasicAuth() - if !ok || user != s.config.StatusUsername || pass != s.config.StatusPassword { + + if !ok || !utils.SecretEqual(user, s.config.StatusUsername) || !utils.SecretEqual(pass, s.config.StatusPassword) { w.Header().Set("Content-Type", "text/plain") w.Header().Set("WWW-Authenticate", `Basic realm="Authenticate"`) w.WriteHeader(http.StatusUnauthorized) @@ -409,7 +411,7 @@ func (s *server) basicAuthRequired(h http.HandlerFunc) http.HandlerFunc { func (s *server) tokenAuthRequired(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") - if !strings.HasPrefix(authHeader, "Bearer ") || authHeader[7:] != s.config.AuthToken { + if !strings.HasPrefix(authHeader, "Bearer ") || !utils.SecretEqual(authHeader[7:], s.config.AuthToken) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Unauthorized")) diff --git a/utils/misc.go b/utils/misc.go index baaaf362d..7a81b1a0a 100644 --- a/utils/misc.go +++ b/utils/misc.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" + "crypto/subtle" "encoding/hex" "net/url" "path" @@ -146,3 +147,8 @@ func MapUpdate[K comparable, V comparable, M ~map[K]V](m1 M, m2 M) { } } } + +// SecretEqual checks if an incoming secret matches the expected secret using constant time comparison. +func SecretEqual(in, expected string) bool { + return subtle.ConstantTimeCompare([]byte(in), []byte(expected)) == 1 +} diff --git a/utils/misc_test.go b/utils/misc_test.go index 1f9c4a163..0b26668fd 100644 --- a/utils/misc_test.go +++ b/utils/misc_test.go @@ -139,3 +139,9 @@ func TestMapUpdate(t *testing.T) { assert.Equal(t, tc.updated, tc.m1) } } + +func TestSecretEqual(t *testing.T) { + assert.True(t, utils.SecretEqual("sesame", "sesame")) + assert.False(t, utils.SecretEqual("sesame", "Sesame")) + assert.False(t, utils.SecretEqual("sesame", "sesame3")) +}