Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
158 changes: 132 additions & 26 deletions docs/middleware/csrf.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,57 @@

### Built-in Extractors

**Secure (Recommended):**
**Most Secure (Recommended):**

- `csrf.FromHeader("X-Csrf-Token")` - Most secure, preferred for APIs
- `csrf.FromForm("_csrf")` - Secure for form submissions
- `csrf.FromHeader("X-Csrf-Token")` - Headers are not logged and cannot be manipulated via URL
- `csrf.FromForm("_csrf")` - Form data is secure and not typically logged

**Acceptable:**
**Less Secure (Use with caution):**

- `csrf.FromQuery("csrf_token")` - URL parameters
- `csrf.FromParam("csrf")` - Route parameters
- `csrf.FromQuery("csrf_token")` - URLs may be logged by servers, proxies, browsers
- `csrf.FromParam("csrf")` - URLs may be logged by servers, proxies, browsers

**Advanced:**

- `csrf.Chain(...)` - Try multiple extractors in sequence

:::note What about cookies?
**Cookies are generally not a secure source for CSRF tokens.** The middleware does not provide a built-in cookie extractor because reading the CSRF token from a cookie with the same name as the CSRF cookie defeats CSRF protection.

**Advanced usage:**
In rare cases, you may securely extract a CSRF token from a cookie if:
- You read from a different cookie (not the CSRF cookie itself)

Check failure on line 238 in docs/middleware/csrf.md

View workflow job for this annotation

GitHub Actions / markdownlint

Lists should be surrounded by blank lines

docs/middleware/csrf.md:238 MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "- You read from a different co..."] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md032.md
- You use multiple cookies for custom validation
- You implement custom logic across different cookie sources

If you do this, set the extractor’s `Source` to `SourceCookie` and allow the middleware to check that the cookie name is different from your CSRF cookie. It will panic if this is the case.

Comment thread
sixcolors marked this conversation as resolved.
**Warning:**
We strongly discourage cookie-based extraction, as it is easy to misconfigure and creates security risks. Prefer extracting tokens from headers or form fields for robust CSRF protection.
:::

### Extractor Metadata

Each extractor returns an `Extractor` struct with metadata about its behavior:

```go
extractor := csrf.FromHeader("X-Csrf-Token")
fmt.Printf("Source: %v, Key: %s", extractor.Source, extractor.Key)
// Output: Source: 0, Key: X-Csrf-Token

// Available source types:
// - csrf.SourceHeader (0): Most secure, not logged
Comment thread
ReneWerner87 marked this conversation as resolved.
// - csrf.SourceForm (1): Secure, not typically logged
// - csrf.SourceQuery (2): Less secure, URLs may be logged
// - csrf.SourceParam (3): Less secure, URLs may be logged
// - csrf.SourceCookie (4): Not recommended for CSRF, no built-in extractor for this source
// - csrf.SourceCustom (5): Security depends on implementation

// Check source type
if extractor.Source == csrf.SourceHeader {
fmt.Println("Using secure header extraction")
}
```

#### Using Route-Specific Extractors

Expand All @@ -246,15 +288,19 @@

### Custom Extractor

You can create a custom extractor to handle specific cases:
You can create a custom extractor to handle specific cases by creating an `Extractor` struct:

:::danger Never Extract from Cookies
**NEVER create custom extractors that read from cookies using the same `CookieName` as your CSRF configuration.** This completely defeats CSRF protection by making the extracted token always match the cookie value, allowing any CSRF attack to succeed.

```go
// ❌ NEVER DO THIS - Completely defeats CSRF protection
func BadExtractor(c fiber.Ctx) (string, error) {
return c.Cookies("csrf_"), nil // Always passes validation!
badExtractor := csrf.Extractor{
Extract: func(c fiber.Ctx) (string, error) {
return c.Cookies("csrf_"), nil // Always passes validation!
},
Source: csrf.SourceCustom,
Key: "csrf_",
}

// ✅ DO THIS - Extract from different source than cookie
Expand All @@ -272,34 +318,84 @@
```go
// Extract CSRF token embedded in JWT Authorization header
// Useful for APIs that combine JWT auth with CSRF protection
func BearerTokenExtractor(c fiber.Ctx) (string, error) {
// Extract from "Authorization: Bearer <jwt>:<csrf>"
auth := c.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
return "", csrf.ErrTokenNotFound
func BearerTokenExtractor() csrf.Extractor {
return csrf.Extractor{
Extract: func(c fiber.Ctx) (string, error) {
// Extract from "Authorization: Bearer <jwt>:<csrf>"
auth := c.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
return "", csrf.ErrTokenNotFound
}

parts := strings.SplitN(strings.TrimPrefix(auth, "Bearer "), ":", 2)
if len(parts) != 2 || parts[1] == "" {
return "", csrf.ErrTokenNotFound
}

return parts[1], nil
},
Source: csrf.SourceCustom,
Key: "Authorization",
}

parts := strings.SplitN(strings.TrimPrefix(auth, "Bearer "), ":", 2)
if len(parts) != 2 || parts[1] == "" {
return "", csrf.ErrTokenNotFound
}

// Usage
app.Use(csrf.New(csrf.Config{
Extractor: BearerTokenExtractor(),
}))
```

#### Custom JSON Body Extractor

```go
// Extract CSRF token from JSON request body
// Useful for APIs that need token in request payload
func JSONBodyExtractor(field string) csrf.Extractor {
return csrf.Extractor{
Extract: func(c fiber.Ctx) (string, error) {
var body map[string]interface{}
if err := c.BodyParser(&body); err != nil {
return "", csrf.ErrTokenNotFound
}

token, ok := body[field].(string)
if !ok || token == "" {
return "", csrf.ErrTokenNotFound
}

return token, nil
},
Source: csrf.SourceCustom,
Key: field,
}

return parts[1], nil
}

// Usage
app.Use(csrf.New(csrf.Config{
Extractor: JSONBodyExtractor("csrf_token"),
}))
```

#### Chain Extractor (Advanced)
### Chain Extractor (Advanced)

For edge cases requiring multiple token sources, use the `Chain` extractor:
For specific cases requiring fallback behavior:

```go
// Only if you absolutely need multiple sources
// Try header first, fallback to form
app.Use(csrf.New(csrf.Config{
Extractor: csrf.Chain(
csrf.FromHeader("X-Csrf-Token"), // Try header first
csrf.FromForm("_csrf"), // Fallback to form
csrf.FromHeader("X-Csrf-Token"),
csrf.FromForm("_csrf"),
),
}))

// Check chain metadata
chained := csrf.Chain(
csrf.FromHeader("X-Csrf-Token"),
csrf.FromForm("_csrf"),
)
fmt.Printf("Primary source: %v, Chain length: %d", chained.Source, len(chained.Chain))
// Output: Primary source: 0, Chain length: 2
```

:::danger Security Risk
Expand Down Expand Up @@ -396,7 +492,7 @@
| IdleTimeout | `time.Duration` | Token expiration time | `30 * time.Minute` |
| KeyGenerator | `func() string` | Token generation function | `utils.UUIDv4` |
| ErrorHandler | `fiber.ErrorHandler` | Custom error handler | `defaultErrorHandler` |
| Extractor | `func(fiber.Ctx) (string, error)` | Token extraction method | `FromHeader("X-Csrf-Token")` |
| Extractor | `csrf.Extractor` | Token extraction method with metadata | `FromHeader("X-Csrf-Token")` |
| Session | `*session.Store` | Session store (**recommended for production**) | `nil` |
| Storage | `fiber.Storage` | Token storage (overridden by Session) | `nil` |
| TrustedOrigins | `[]string` | Trusted origins for cross-origin requests | `[]` |
Expand All @@ -422,4 +518,14 @@
const (
HeaderName = "X-Csrf-Token"
)

// Source types for extractor metadata
const (
SourceHeader Source = iota // 0 - Most secure
SourceForm // 1 - Secure
SourceQuery // 2 - Less secure
Comment thread
ReneWerner87 marked this conversation as resolved.
SourceParam // 3 - Less secure
SourceCookie // 4 - Not recommended for CSRF, no built-in extractor for this source
SourceCustom // 5 - Security depends on implementation
)
```
73 changes: 62 additions & 11 deletions middleware/csrf/config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package csrf

import (
"fmt"
"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"
)
Expand Down Expand Up @@ -37,16 +40,6 @@ type Config struct {
// Optional. Default: defaultErrorHandler
ErrorHandler fiber.ErrorHandler

// Extractor returns the CSRF token from the request.
//
// Optional. Default: FromHeader("X-Csrf-Token")
//
// 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)

// CookieName is the name of the CSRF cookie.
//
// Optional. Default: "csrf_"
Expand Down Expand Up @@ -80,6 +73,21 @@ type Config struct {
// Optional. Default: []
TrustedOrigins []string

// Extractor returns the CSRF token from the request.
//
// Optional. Default: FromHeader("X-Csrf-Token")
//
// Available extractors:
// - FromHeader: Most secure, recommended for APIs
// - FromForm: Secure, recommended for form submissions
// - FromQuery: Less secure, URLs may be logged
// - FromParam: Less secure, URLs may be logged
// - Chain: Advanced chaining of multiple extractors
//
// WARNING: Never create custom extractors that read from cookies with the same
// CookieName as this defeats CSRF protection entirely.
Extractor Extractor

// IdleTimeout is the duration of time the CSRF token is valid.
//
// Optional. Default: 30 * time.Minute
Expand Down Expand Up @@ -152,9 +160,52 @@ func configDefault(config ...Config) Config {
if cfg.ErrorHandler == nil {
cfg.ErrorHandler = ConfigDefault.ErrorHandler
}
if cfg.Extractor == nil {
// Check if Extractor is zero value (since it's a struct)
if cfg.Extractor.Extract == nil {
cfg.Extractor = ConfigDefault.Extractor
}
// Validate extractor security configurations
validateExtractorSecurity(cfg)

return cfg
}

// validateExtractorSecurity checks for insecure extractor configurations
func validateExtractorSecurity(cfg Config) {
// Check primary extractor
if isInsecureCookieExtractor(cfg.Extractor, cfg.CookieName) {
panic("CSRF: Extractor reads from the same cookie '" + cfg.CookieName +
"' used for token storage. This completely defeats CSRF protection.")
}

// Check chained extractors
for i, extractor := range cfg.Extractor.Chain {
if isInsecureCookieExtractor(extractor, cfg.CookieName) {
panic(fmt.Sprintf("CSRF: Chained extractor #%d reads from the same cookie '%s' "+
"used for token storage. This completely defeats CSRF protection.", i+1, cfg.CookieName))
}
}

// Additional security warnings (non-fatal)
if cfg.Extractor.Source == SourceQuery || cfg.Extractor.Source == SourceParam {
log.Warn("[CSRF WARNING] Using %v extractor - URLs may be logged", cfg.Extractor.Source)
}
Comment thread
sixcolors marked this conversation as resolved.
}

// isInsecureCookieExtractor checks if an extractor unsafely reads from the CSRF cookie
func isInsecureCookieExtractor(extractor Extractor, cookieName string) bool {
if extractor.Source == SourceCookie {
// Exact match - definitely insecure
if extractor.Key == cookieName {
return true
}

// Case-insensitive match - potentially confusing, warn but don't panic
if strings.EqualFold(extractor.Key, cookieName) && extractor.Key != cookieName {
log.Warn("[CSRF WARNING] Extractor cookie name '%s' is similar to CSRF cookie '%s' - this may be confusing",
extractor.Key, cookieName)
}
}

return false
}
Loading
Loading