Repository: labstack/echo (go)

Issue:
The CSRF cookie is not set when opening a page through an iframe
### Issue Description

There is no way to change `SameSite` mode when setting CSRF cookies.

### Checklist

- [x] Dependencies installed
- [x] No typos
- [x] Searched existing issues and docs

### Expected behaviour

Successfully setting CSRF cookie when opening a page through an iframe.

### Actual behaviour

Cannot install CSRF via iframe. The browser reports an error: 
`This set-cookie didn't specify a SameSite attribute and was defaulted to SameSite=Lax and broke the same rules specified in the SameSiteLax value.`

### Steps to reproduce

Activate the `SameSite` experimental mode in Google Chrome (which will be turned on by default soon) and try to open the page through the iframe.

How to enable: https://www.chromium.org/updates/same-site/test-debug
What is a problem: https://web.dev/samesite-cookies-explained/

### Working code to debug

```go
server.Echo.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
	TokenLookup: "header:X-XSRF-TOKEN",
	CookieName:  "_csrf",
}))
```

### Version/commit

v4.1.15


Pull request:
Fix #1523 by adding SameSite mode for CSRF settings

Patch:
diff --git a/middleware/csrf.go b/middleware/csrf.go
index 09a66bb..7804997 100644
--- a/middleware/csrf.go
+++ b/middleware/csrf.go
@@ -57,6 +57,10 @@ type (
 		// Indicates if CSRF cookie is HTTP only.
 		// Optional. Default value false.
 		CookieHTTPOnly bool `yaml:"cookie_http_only"`
+
+		// Indicates SameSite mode of the CSRF cookie.
+		// Optional. Default value SameSiteDefaultMode.
+		CookieSameSite http.SameSite `yaml:"cookie_same_site"`
 	}
 
 	// csrfTokenExtractor defines a function that takes `echo.Context` and returns
@@ -67,12 +71,13 @@ type (
 var (
 	// DefaultCSRFConfig is the default CSRF middleware config.
 	DefaultCSRFConfig = CSRFConfig{
-		Skipper:      DefaultSkipper,
-		TokenLength:  32,
-		TokenLookup:  "header:" + echo.HeaderXCSRFToken,
-		ContextKey:   "csrf",
-		CookieName:   "_csrf",
-		CookieMaxAge: 86400,
+		Skipper:        DefaultSkipper,
+		TokenLength:    32,
+		TokenLookup:    "header:" + echo.HeaderXCSRFToken,
+		ContextKey:     "csrf",
+		CookieName:     "_csrf",
+		CookieMaxAge:   86400,
+		CookieSameSite: http.SameSiteDefaultMode,
 	}
 )
 
@@ -105,6 +110,9 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 	if config.CookieMaxAge == 0 {
 		config.CookieMaxAge = DefaultCSRFConfig.CookieMaxAge
 	}
+	if config.CookieSameSite == http.SameSiteNoneMode {
+		config.CookieSecure = true
+	}
 
 	// Initialize
 	parts := strings.Split(config.TokenLookup, ":")
@@ -157,6 +165,9 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 			if config.CookieDomain != "" {
 				cookie.Domain = config.CookieDomain
 			}
+			if config.CookieSameSite != http.SameSiteDefaultMode {
+				cookie.SameSite = config.CookieSameSite
+			}
 			cookie.Expires = time.Now().Add(time.Duration(config.CookieMaxAge) * time.Second)
 			cookie.Secure = config.CookieSecure
 			cookie.HttpOnly = config.CookieHTTPOnly


Test patch:
diff --git a/middleware/csrf_test.go b/middleware/csrf_test.go
index efb4dd1..af1d263 100644
--- a/middleware/csrf_test.go
+++ b/middleware/csrf_test.go
@@ -1,6 +1,7 @@
 package middleware
 
 import (
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -81,3 +82,79 @@ func TestCSRFTokenFromQuery(t *testing.T) {
 	assert.Error(t, err)
 	csrfTokenFromQuery("csrf")
 }
+
+func TestCSRFSetSameSiteMode(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{
+		CookieSameSite: http.SameSiteStrictMode,
+	})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	assert.Regexp(t, "SameSite=Strict", rec.Header()["Set-Cookie"])
+}
+
+func TestCSRFWithoutSameSiteMode(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"])
+}
+
+func TestCSRFWithSameSiteDefaultMode(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{
+		CookieSameSite: http.SameSiteDefaultMode,
+	})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	fmt.Println(rec.Header()["Set-Cookie"])
+	assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"])
+}
+
+func TestCSRFWithSameSiteModeNone(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{
+		CookieSameSite: http.SameSiteNoneMode,
+	})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	assert.Regexp(t, "SameSite=None", rec.Header()["Set-Cookie"])
+	assert.Regexp(t, "Secure", rec.Header()["Set-Cookie"])
+}