-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(csrf): Enhance extractor functionality with metadata and security validation #3630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| - 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. | ||
|
|
||
| **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 | ||
|
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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | `[]` | | ||
|
|
@@ -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 | ||
|
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 | ||
| ) | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.