From 54b6cb17504ca49ce0bb354a8ed20012d7246169 Mon Sep 17 00:00:00 2001 From: Harry Tran Date: Wed, 30 Sep 2026 19:28:05 +0700 Subject: [PATCH] fix(middleware): reject empty tokens in CSRF validation subtle.ConstantTimeCompare treats two empty byte slices as equal, so validateCSRFToken("", "") returns true. Extractors in extractor.go treat a present-but-empty query/form/header value as successfully extracted, so an attacker can supply an empty client token with no special access. If the stored cookie token is ever empty (e.g. an app clears it to "" instead of expiring it, or CookieSecure is false and the cookie is overwritten over plain HTTP), CSRF validation is bypassed entirely. Reject empty tokens before the comparison. --- middleware/csrf.go | 3 +++ middleware/csrf_test.go | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/middleware/csrf.go b/middleware/csrf.go index 3999a92c9..e6359ff4c 100644 --- a/middleware/csrf.go +++ b/middleware/csrf.go @@ -256,6 +256,9 @@ func (config CSRFConfig) ToMiddleware() (echo.MiddlewareFunc, error) { } func validateCSRFToken(token, clientToken string) bool { + if token == "" || clientToken == "" { + return false + } return subtle.ConstantTimeCompare([]byte(token), []byte(clientToken)) == 1 } diff --git a/middleware/csrf_test.go b/middleware/csrf_test.go index 1a6b18eda..433c14bf2 100644 --- a/middleware/csrf_test.go +++ b/middleware/csrf_test.go @@ -278,6 +278,16 @@ func TestCSRFWithConfig(t *testing.T) { expectEmptyBody: true, expectErr: `code=403, message=invalid csrf token`, }, + { + name: "nok, POST empty cookie token + empty client token is blocked", + whenHeaders: map[string]string{ + echo.HeaderCookie: "_csrf=", + echo.HeaderXCSRFToken: "", + }, + whenMethod: http.MethodPost, + expectEmptyBody: true, + expectErr: `code=403, message=invalid csrf token`, + }, { name: "nok, invalid trusted origin in Config", givenConfig: &CSRFConfig{