Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e408522
refactor: simplify CSRF config by removing KeyLookup and enhancing Ex…
sixcolors Jul 18, 2025
3b7a394
feat: add support for route-specific CSRF extractors and implement Ch…
sixcolors Jul 18, 2025
c3caf18
fix: update Extractor documentation to reflect optional status
sixcolors Jul 18, 2025
7c7e926
fix: update CSRF documentation to clarify extractor recommendations a…
sixcolors Jul 18, 2025
4bee3a1
fix: improve cookie extractor validation and enhance config warnings
sixcolors Jul 18, 2025
0cb8ae1
fix: remove cookie extractor and update documentation to enhance CSRF…
sixcolors Jul 19, 2025
e4a48f9
fix: enhance CSRF middleware documentation for clarity and accuracy
sixcolors Jul 19, 2025
7a03b1a
fix: improve error messages and documentation for CSRF middleware ext…
sixcolors Jul 19, 2025
b29c322
fix: improve test parallelism and cleanup in CSRF middleware tests
sixcolors Jul 19, 2025
5e61845
fix: enhance CSRF extractor tests with proper context setup for error…
sixcolors Jul 19, 2025
ad6007d
fix: simplify error handling in Chain function by removing unnecessar…
sixcolors Jul 19, 2025
d620651
fix: remove deprecated fields from CSRF middleware configuration for …
sixcolors Jul 19, 2025
d13c974
chore: gofumpt
sixcolors Jul 19, 2025
2d819c3
fix: reorder fields
sixcolors Jul 19, 2025
46b7c24
fix: update CSRF middleware documentation for improved clarity and se…
sixcolors Jul 19, 2025
6a6b587
fix: improve clarity in CSRF middleware documentation regarding custo…
sixcolors Jul 19, 2025
b94784c
fix: enhance CSRF extractors tests for improved coverage and error ha…
sixcolors Jul 19, 2025
0bf5b16
fix: add missing newlines for improved markdown formatting
sixcolors Jul 19, 2025
0571e16
Update csrf.md
ReneWerner87 Jul 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ markdown:
## lint: 🚨 Run lint checks
.PHONY: lint
lint:
golangci-lint run
golangci-lint run

## modernize: 🛠 Run gopls modernize
.PHONY: modernize
Expand Down
512 changes: 303 additions & 209 deletions docs/middleware/csrf.md

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,42 @@ app.Use(csrf.New(csrf.Config{

- **Session Key Removal**: The `SessionKey` field has been removed from the CSRF middleware configuration. The session key is now an unexported constant within the middleware to avoid potential key collisions in the session store.

- **KeyLookup Field Removal**: The `KeyLookup` field has been removed from the CSRF middleware configuration. This field was deprecated and is no longer needed as the middleware now uses a more secure approach for token management.

```go
// Before
app.Use(csrf.New(csrf.Config{
KeyLookup: "header:X-CSRF-Token",
// other config...
}))

// After - use Extractor instead
app.Use(csrf.New(csrf.Config{
Extractor: csrf.FromHeader("X-CSRF-Token"),
// other config...
}))
```

- **FromCookie Extractor Removal**: The `csrf.FromCookie` extractor has been intentionally removed for security reasons. Using cookie-based extraction defeats the purpose of CSRF protection by making the extracted token always match the cookie value.

```go
// Before - This was a security vulnerability
app.Use(csrf.New(csrf.Config{
Extractor: csrf.FromCookie("csrf_token"), // ❌ Insecure!
}))

// After - Use secure extractors instead
app.Use(csrf.New(csrf.Config{
Extractor: csrf.FromHeader("X-Csrf-Token"), // ✅ Secure
// or
Extractor: csrf.FromForm("_csrf"), // ✅ Secure
// or
Extractor: csrf.FromQuery("csrf_token"), // ✅ Acceptable
}))
```

**Security Note**: The removal of `FromCookie` prevents a common misconfiguration that would completely bypass CSRF protection. The middleware uses the Double Submit Cookie pattern, which requires the token to be submitted through a different channel than the cookie to provide meaningful protection.

#### Filesystem

You need to move filesystem middleware to static middleware due to it has been removed from the core.
Expand Down
124 changes: 43 additions & 81 deletions middleware/csrf/config.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package csrf

import (
"net/textproto"
"strings"
"time"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/log"
"github.com/gofiber/fiber/v3/middleware/session"
"github.com/gofiber/utils/v2"
)

// Config defines the config for middleware.
// Config defines the config for CSRF middleware.
type Config struct {
// Store is used to store the state of the middleware
// Storage is used to store the state of the middleware.
//
// Optional. Default: memory.New()
// Ignored if Session is set.
Expand All @@ -24,69 +21,61 @@ type Config struct {
// Optional. Default: nil
Next func(c fiber.Ctx) bool

// Session is used to store the state of the middleware
// Session is used to store the state of the middleware.
//
// Optional. Default: nil
// If set, the middleware will use the session store instead of the storage
// If set, the middleware will use the session store instead of the storage.
Session *session.Store

// KeyGenerator creates a new CSRF token
// KeyGenerator creates a new CSRF token.
//
// Optional. Default: utils.UUID
// Optional. Default: utils.UUIDv4
KeyGenerator func() string

// ErrorHandler is executed when an error is returned from fiber.Handler.
//
// Optional. Default: DefaultErrorHandler
// Optional. Default: defaultErrorHandler
ErrorHandler fiber.ErrorHandler

// Extractor returns the csrf token
// Extractor returns the CSRF token from the request.
//
// If set this will be used in place of an Extractor based on KeyLookup.
// Optional. Default: FromHeader("X-Csrf-Token")
//
// Optional. Default will create an Extractor based on KeyLookup.
// Available extractors: FromHeader, FromQuery, FromParam, FromForm
//
// WARNING: Never create custom extractors that read from cookies with the same
// CookieName as this defeats CSRF protection entirely.
Extractor func(c fiber.Ctx) (string, error)

// KeyLookup is a string in the form of "<source>:<key>" that is used
// to create an Extractor that extracts the token from the request.
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "param:<name>"
// - "form:<name>"
// - "cookie:<name>"
//
// Ignored if an Extractor is explicitly set.
// CookieName is the name of the CSRF cookie.
//
// Optional. Default: "header:X-Csrf-Token"
KeyLookup string

// Name of the session cookie. This cookie will store session key.
// Optional. Default value "csrf_".
// Overridden if KeyLookup == "cookie:<name>"
// Optional. Default: "csrf_"
CookieName string

// Domain of the CSRF cookie.
// Optional. Default value "".
// CookieDomain is the domain of the CSRF cookie.
//
// Optional. Default: ""
CookieDomain string

// Path of the CSRF cookie.
// Optional. Default value "".
// CookiePath is the path of the CSRF cookie.
//
// Optional. Default: ""
CookiePath string

// Value of SameSite cookie.
// Optional. Default value "Lax".
// CookieSameSite is the SameSite attribute of the CSRF cookie.
//
// Optional. Default: "Lax"
CookieSameSite string

// TrustedOrigins is a list of trusted origins for unsafe requests.
// For requests that use the Origin header, the origin must match the
// Host header or one of the TrustedOrigins.
// For secure requests, that do not include the Origin header, the Referer
// For secure requests that do not include the Origin header, the Referer
// header must match the Host header or one of the TrustedOrigins.
//
// This supports matching subdomains at any level. This means you can use a value like
// `"https://*.example.com"` to allow any subdomain of `example.com` to submit requests,
// including multiple subdomain levels such as `"https://sub.sub.example.com"`.
// "https://*.example.com" to allow any subdomain of example.com to submit requests,
// including multiple subdomain levels such as "https://sub.sub.example.com".
//
// Optional. Default: []
TrustedOrigins []string
Expand All @@ -96,30 +85,34 @@ type Config struct {
// Optional. Default: 30 * time.Minute
IdleTimeout time.Duration

// Indicates if CSRF cookie is secure.
// Optional. Default value false.
// CookieSecure indicates if CSRF cookie is secure.
//
// Optional. Default: false
CookieSecure bool

// Indicates if CSRF cookie is HTTP only.
// Optional. Default value false.
// CookieHTTPOnly indicates if CSRF cookie is HTTP only.
//
// Optional. Default: false
CookieHTTPOnly bool

// Decides whether cookie should last for only the browser sesison.
// Ignores Expiration if set to true
// CookieSessionOnly decides whether cookie should last for only the browser session.
// Ignores Expiration if set to true.
//
// Optional. Default: false
CookieSessionOnly bool

// SingleUseToken indicates if the CSRF token be destroyed
// SingleUseToken indicates if the CSRF token should be destroyed
// and a new one generated on each use.
//
// Optional. Default: false
SingleUseToken bool
}

// HeaderName is the default header name for CSRF tokens.
const HeaderName = "X-Csrf-Token"

// ConfigDefault is the default config
// ConfigDefault is the default config for CSRF middleware.
var ConfigDefault = Config{
KeyLookup: "header:" + HeaderName,
CookieName: "csrf_",
CookieSameSite: "Lax",
IdleTimeout: 30 * time.Minute,
Expand All @@ -128,12 +121,12 @@ var ConfigDefault = Config{
Extractor: FromHeader(HeaderName),
}

// default ErrorHandler that process return error from fiber.Handler
// defaultErrorHandler is the default error handler that processes errors from fiber.Handler.
func defaultErrorHandler(_ fiber.Ctx, _ error) error {
return fiber.ErrForbidden
}

// Helper function to set default values
// configDefault is a helper function to set default values.
func configDefault(config ...Config) Config {
// Return default config if nothing provided
if len(config) < 1 {
Expand All @@ -144,9 +137,6 @@ func configDefault(config ...Config) Config {
cfg := config[0]

// Set default values
if cfg.KeyLookup == "" {
cfg.KeyLookup = ConfigDefault.KeyLookup
}
if cfg.IdleTimeout <= 0 {
cfg.IdleTimeout = ConfigDefault.IdleTimeout
}
Expand All @@ -162,36 +152,8 @@ func configDefault(config ...Config) Config {
if cfg.ErrorHandler == nil {
cfg.ErrorHandler = ConfigDefault.ErrorHandler
}

// Generate the correct extractor to get the token from the correct location
selectors := strings.Split(cfg.KeyLookup, ":")

const numParts = 2
if len(selectors) != numParts {
panic("[CSRF] KeyLookup must in the form of <source>:<key>")
}

if cfg.Extractor == nil {
// By default we extract from a header
cfg.Extractor = FromHeader(textproto.CanonicalMIMEHeaderKey(selectors[1]))

switch selectors[0] {
case "form":
cfg.Extractor = FromForm(selectors[1])
case "query":
cfg.Extractor = FromQuery(selectors[1])
case "param":
cfg.Extractor = FromParam(selectors[1])
case "cookie":
if cfg.Session == nil {
log.Warn("[CSRF] Cookie extractor is not recommended without a session store")
}
if cfg.CookieSameSite == "None" || cfg.CookieSameSite != "Lax" && cfg.CookieSameSite != "Strict" {
log.Warn("[CSRF] Cookie extractor is only recommended for use with SameSite=Lax or SameSite=Strict")
}
cfg.Extractor = FromCookie(selectors[1])
cfg.CookieName = selectors[1] // Cookie name is the same as the key
}
cfg.Extractor = ConfigDefault.Extractor
}

return cfg
Expand Down
20 changes: 8 additions & 12 deletions middleware/csrf/csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package csrf
import (
"errors"
"net/url"
"reflect"
"slices"
"strings"
"time"
Expand All @@ -21,7 +20,8 @@ var (
ErrOriginInvalid = errors.New("origin invalid")
ErrOriginNoMatch = errors.New("origin does not match host and is not a trusted origin")
errOriginNotFound = errors.New("origin not supplied or is null") // internal error, will not be returned to the user
dummyValue = []byte{'+'}
dummyValue = []byte{'+'} // dummyValue is a placeholder value stored in token storage. The actual token validation relies on the key, not this value.

)

// Handler for CSRF middleware
Expand Down Expand Up @@ -130,7 +130,7 @@ func New(config ...Config) fiber.Handler {
return cfg.ErrorHandler(c, err)
}

// Extract token from client request i.e. header, query, param, form or cookie
// Extract token from client request i.e. header, query, param, form
extractedToken, err := cfg.Extractor(c)
if err != nil {
return cfg.ErrorHandler(c, err)
Expand All @@ -140,10 +140,11 @@ func New(config ...Config) fiber.Handler {
return cfg.ErrorHandler(c, ErrTokenNotFound)
}

// If not using FromCookie extractor, check that the token matches the cookie
// This is to prevent CSRF attacks by using a Double Submit Cookie method
// Useful when we do not have access to the users Session
if !isFromCookie(cfg.Extractor) && !compareStrings(extractedToken, c.Cookies(cfg.CookieName)) {
// Double Submit Cookie validation: ensure the extracted token matches the cookie value
// This prevents CSRF attacks by requiring attackers to know both the cookie AND submit
// the same token through a different channel (header, form, etc.)
// WARNING: If using a custom extractor that reads from the same cookie, this provides no protection
if !compareStrings(extractedToken, c.Cookies(cfg.CookieName)) {
return cfg.ErrorHandler(c, ErrTokenInvalid)
}

Expand Down Expand Up @@ -274,11 +275,6 @@ func (handler *Handler) DeleteToken(c fiber.Ctx) error {
return nil
}

// isFromCookie checks if the extractor is set to ExtractFromCookie
func isFromCookie(extractor any) bool {
return reflect.ValueOf(extractor).Pointer() == reflect.ValueOf(FromCookie).Pointer()
}

// originMatchesHost checks that the origin header matches the host header
// returns an error if the origin header is not present or is invalid
// returns nil if the origin header is valid
Expand Down
Loading
Loading