Skip to content
Merged
191 changes: 45 additions & 146 deletions docs/middleware/csrf.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The CSRF middleware protects against [Cross-Site Request Forgery](https://en.wik
```go
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
"github.com/gofiber/fiber/v3/middleware/csrf"
)

Expand All @@ -39,7 +40,7 @@ app.Use(csrf.New(csrf.Config{
CookieHTTPOnly: true, // false for SPAs
CookieSameSite: "Lax",
CookieSessionOnly: true,
Extractor: csrf.FromHeader("X-Csrf-Token"),
Extractor: extractors.FromHeader("X-Csrf-Token"),
Session: sessionStore,
}))
```
Expand Down Expand Up @@ -76,7 +77,7 @@ app.Use(csrf.New(csrf.Config{
CookieHTTPOnly: true, // Secure - blocks JavaScript
CookieSameSite: "Lax",
CookieSessionOnly: true,
Extractor: csrf.FromForm("_csrf"),
Extractor: extractors.FromForm("_csrf"),
Session: sessionStore,
}))
```
Expand All @@ -90,7 +91,7 @@ app.Use(csrf.New(csrf.Config{
CookieHTTPOnly: false, // Required for JavaScript access to tokens
CookieSameSite: "Lax",
CookieSessionOnly: true,
Extractor: csrf.FromHeader("X-Csrf-Token"),
Extractor: extractors.FromHeader("X-Csrf-Token"),
Session: sessionStore,
}))
```
Expand All @@ -111,7 +112,7 @@ SPAs require `CookieHTTPOnly: false` to access tokens via JavaScript. This sligh
```go
func formHandler(c fiber.Ctx) error {
token := csrf.TokenFromContext(c)

return c.SendString(fmt.Sprintf(`
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="%s">
Expand All @@ -127,7 +128,7 @@ func formHandler(c fiber.Ctx) error {
```go
func apiHandler(c fiber.Ctx) error {
token := csrf.TokenFromContext(c)

return c.JSON(fiber.Map{
"csrf_token": token,
"data": "your data",
Expand All @@ -146,7 +147,7 @@ function getCsrfToken() {
// Use with fetch API
async function makeRequest(url, data) {
const csrfToken = getCsrfToken();

const response = await fetch(url, {
method: 'POST',
headers: {
Expand All @@ -155,11 +156,11 @@ async function makeRequest(url, data) {
},
body: JSON.stringify(data)
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return response.json();
}
```
Expand Down Expand Up @@ -214,26 +215,23 @@ As a crucial second layer of defense, the middleware **always** performs `Origin

## Token Extractors

### Built-in Extractors
This middleware uses the shared `extractors` package for token extraction. For full details on extractor types, chaining, security, and advanced usage, see the [Extractors Guide](https://docs.gofiber.io/guide/extractors).

**Most Secure (Recommended):**
**Extractor Source Constants:**
Extractor source constants (such as `SourceHeader`, `SourceForm`, etc.) are defined in the shared extractors package, not in the CSRF middleware itself. Refer to the Extractors Guide for their definitions and usage.

- `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
### CSRF-Specific Extractor Notes

**Less Secure (Use with caution):**
For CSRF protection, prefer secure extraction methods:

- `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
- **Headers** (`extractors.FromHeader("X-Csrf-Token")`) – Most secure, not logged in URLs
- **Form data** (`extractors.FromForm("_csrf")`) – Secure for form submissions
- **Avoid URL parameters** – Query/param extractors expose tokens in logs and browser history

:::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.
**Cookies are generally not a secure source for CSRF tokens.** The middleware will panic if you configure an extractor that reads from cookies with the same name as your CSRF cookie. This is because reading the CSRF token from a cookie with the same name as the CSRF cookie defeats CSRF protection entirely, as the extracted token will always match the cookie value, allowing any CSRF attack to succeed.

**Advanced usage:**
**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)
Expand All @@ -242,54 +240,31 @@ In rare cases, you may securely extract a CSRF token from a cookie if:

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.

**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.
**Warning:**
Cookie-based extraction is strongly discouraged, as it is easy to misconfigure and creates security risks. Prefer extracting tokens from headers or form fields for robust CSRF protection. See the [Extractors Guide](https://docs.gofiber.io/guide/extractors#security-considerations) for more details.
:::

### 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
// - 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")
}
```
### Route-Specific Configuration

#### Using Route-Specific Extractors

There are cases where you might want to use different extractors for different routes:
You can configure different extraction methods for different routes:

```go
// API routes - header only
// API routes - header extraction for AJAX/fetch requests
api := app.Group("/api")
api.Use(csrf.New(csrf.Config{
Extractor: csrf.FromHeader("X-Csrf-Token"),
Extractor: extractors.FromHeader("X-Csrf-Token"),
}))

// Form routes - form only
// Form routes - form field extraction for traditional forms
forms := app.Group("/forms")
forms.Use(csrf.New(csrf.Config{
Extractor: csrf.FromForm("_csrf"),
Extractor: extractors.FromForm("_csrf"),
}))
```

### Custom Extractor
### Custom CSRF Extractors

You can create a custom extractor to handle specific cases by creating an `Extractor` struct:
For specialized CSRF token extraction needs, you can create custom extractors. See the [Extractors Guide](https://docs.gofiber.io/guide/extractors#custom-extractors) for advanced patterns and security notes.

:::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.
Expand All @@ -300,107 +275,40 @@ badExtractor := csrf.Extractor{
Extract: func(c fiber.Ctx) (string, error) {
return c.Cookies("csrf_"), nil // Always passes validation!
},
Source: csrf.SourceCustom,
Source: csrf.SourceCustom, // See extractors.SourceCustom in shared package
Key: "csrf_",
}

// ✅ DO THIS - Extract from different source than cookie
app.Use(csrf.New(csrf.Config{
CookieName: "csrf_",
Extractor: csrf.FromHeader("X-Csrf-Token"), // Header vs cookie comparison
Extractor: extractors.FromHeader("X-Csrf-Token"), // Header vs cookie comparison
}))
```

The middleware uses the **Double Submit Cookie** pattern - it compares the extracted token against the cookie value. If your extractor reads from the same cookie, they will always match and provide zero CSRF protection.
The middleware uses the **Double Submit Cookie** pattern it compares the extracted token against the cookie value. If you configure an extractor that reads from the same cookie, it will panic because they will always match and provide zero CSRF protection.
:::

#### Bearer Token Embedding

```go
// Extract CSRF token embedded in JWT Authorization header
// Useful for APIs that combine JWT auth with CSRF protection
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",
}
}

// 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,
}
}
#### Bearer Token Embedding & Custom Extractors

// Usage
app.Use(csrf.New(csrf.Config{
Extractor: JSONBodyExtractor("csrf_token"),
}))
```
You can create advanced extractors for use cases like JWT embedding or JSON body parsing. See the [Extractors Guide](https://docs.gofiber.io/guide/extractors#custom-extractors) for secure implementation patterns and more examples.

### Chain Extractor (Advanced)
### Fallback Extraction

For specific cases requiring fallback behavior:
For applications that need to support both AJAX and form submissions:

```go
// Try header first, fallback to form
// Try header first (AJAX), fallback to form (traditional forms)
app.Use(csrf.New(csrf.Config{
Extractor: csrf.Chain(
csrf.FromHeader("X-Csrf-Token"),
csrf.FromForm("_csrf"),
Extractor: extractors.Chain(
extractors.FromHeader("X-Csrf-Token"),
extractors.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
Chaining extractors increases attack surface and complexity. Most applications should use a single, appropriate extractor for their use case.
:::warning
Chaining extractors increases complexity. Use only when you need to support multiple client types. See the [Extractors Guide](https://docs.gofiber.io/guide/extractors#chaining-extractors) for details and security notes.
:::

## Advanced Configuration
Expand Down Expand Up @@ -459,7 +367,8 @@ if handler != nil {
}

// With session middleware
session.Destroy() // Also deletes CSRF token
// Destroying the session will also remove the CSRF token if using session-based CSRF.
session.Destroy()
```
Comment on lines 369 to 372
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct session-destruction example and lifecycle

session.Destroy() as a bare call is unclear. Show destruction via context and recommend releasing the session.

- // Destroying the session will also remove the CSRF token if using session-based CSRF.
- session.Destroy()
+ // Destroying the session will also remove the CSRF token if using session-based CSRF.
+ sess := session.FromContext(c)
+ if err := sess.Destroy(); err != nil {
+     // handle error
+ }
+ sess.Release() // return to pool

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In docs/middleware/csrf.md around lines 369 to 372, the example uses a bare
session.Destroy() which is ambiguous; update the example to show destroying the
session with the request context (e.g., session.Destroy(ctx)) and demonstrate
releasing/saving the session afterwards (call the session's Release/Save/Close
method as appropriate) so readers see the full lifecycle: obtain session from
context, destroy using the same context, and then release the session to avoid
resource leaks.


## API Reference
Expand Down Expand Up @@ -493,7 +402,7 @@ func (h *csrf.Handler) DeleteToken(c fiber.Ctx) error
| 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 | `csrf.Extractor` | Token extraction method with metadata | `FromHeader("X-Csrf-Token")` |
| Extractor | `extractors.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 @@ -519,14 +428,4 @@ var (
const (
HeaderName = "X-Csrf-Token"
)

// Source types for extractor metadata
const (
SourceHeader Source = iota // 0 - Most secure
SourceForm // 1 - Secure
SourceQuery // 2 - Less secure
SourceParam // 3 - Less secure
SourceCookie // 4 - Not recommended for CSRF, no built-in extractor for this source
SourceCustom // 5 - Security depends on implementation
)
```
28 changes: 16 additions & 12 deletions middleware/csrf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
"github.com/gofiber/fiber/v3/log"
"github.com/gofiber/fiber/v3/middleware/session"
utils "github.com/gofiber/utils/v2"
Expand Down Expand Up @@ -75,18 +76,21 @@ type Config struct {

// Extractor returns the CSRF token from the request.
//
// Optional. Default: FromHeader("X-Csrf-Token")
// Optional. Default: extractors.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
// Available extractors from github.com/gofiber/fiber/v3/extractors:
// - extractors.FromHeader("X-Csrf-Token"): Most secure, recommended for APIs
// - extractors.FromForm("_csrf"): Secure, recommended for form submissions
// - extractors.FromQuery("csrf_token"): Less secure, URLs may be logged
// - extractors.FromParam("csrf"): Less secure, URLs may be logged
// - extractors.Chain(...): Advanced chaining of multiple extractors
//
// See the Extractors Guide for complete documentation:
// https://docs.gofiber.io/guide/extractors
//
// WARNING: Never create custom extractors that read from cookies with the same
// CookieName as this defeats CSRF protection entirely.
Extractor Extractor
Extractor extractors.Extractor

// IdleTimeout is the duration of time the CSRF token is valid.
//
Expand Down Expand Up @@ -126,7 +130,7 @@ var ConfigDefault = Config{
IdleTimeout: 30 * time.Minute,
KeyGenerator: utils.UUIDv4,
ErrorHandler: defaultErrorHandler,
Extractor: FromHeader(HeaderName),
Extractor: extractors.FromHeader(HeaderName),
}

// defaultErrorHandler is the default error handler that processes errors from fiber.Handler.
Expand Down Expand Up @@ -187,14 +191,14 @@ func validateExtractorSecurity(cfg Config) {
}

// Additional security warnings (non-fatal)
if cfg.Extractor.Source == SourceQuery || cfg.Extractor.Source == SourceParam {
if cfg.Extractor.Source == extractors.SourceQuery || cfg.Extractor.Source == extractors.SourceParam {
log.Warnf("[CSRF WARNING] Using %v extractor - URLs may be logged", cfg.Extractor.Source)
}
}

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