mirror of
https://github.com/gofiber/fiber.git
synced 2025-02-14 13:46:23 +00:00
Merge pull request from GHSA-94w9-97p3-p368
* feat: improved csrf with session support * fix: double submit cookie * feat: add warning cookie extractor without session * feat: add warning CsrfFromCookie SameSite * fix: use byes.Equal instead * fix: Overriden CookieName KeyLookup cookie:<name> * feat: Create helpers.go * feat: use compareTokens (constant time compare) * feat: validate cookie to prevent token injection * refactor: clean up csrf.go * docs: update comment about Double Submit Cookie * docs: update docs for CSRF changes * feat: add DeleteToken * refactor: no else * test: add more tests * refactor: re-order tests * docs: update safe methods RCF add note * test: add CSRF_Cookie_Injection_Exploit * feat: add SingleUseToken config * test: check for new token * docs: use warning * fix: always register type Token * feat: use UUIDv4 * test: swap in UUIDv4 here too
This commit is contained in:
parent
9292a36e28
commit
b50d91d58e
@ -4,7 +4,7 @@ id: csrf
|
||||
|
||||
# CSRF
|
||||
|
||||
CSRF middleware for [Fiber](https://github.com/gofiber/fiber) that provides [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by passing a csrf token via cookies. This cookie value will be used to compare against the client csrf token on requests, other than those defined as "safe" by RFC7231 \(GET, HEAD, OPTIONS, or TRACE\). When the csrf token is invalid, this middleware will return the `fiber.ErrForbidden` error.
|
||||
CSRF middleware for [Fiber](https://github.com/gofiber/fiber) that provides [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by passing a csrf token via cookies. This cookie value will be used to compare against the client csrf token on requests, other than those defined as "safe" by [RFC9110#section-9.2.1](https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.2.1) \(GET, HEAD, OPTIONS, or TRACE\). When the csrf token is invalid, this middleware will return the `fiber.ErrForbidden` error.
|
||||
|
||||
CSRF Tokens are generated on GET requests. You can retrieve the CSRF token with `c.Locals(contextKey)`, where `contextKey` is the string you set in the config (see Custom Config below).
|
||||
|
||||
@ -14,6 +14,70 @@ When no `csrf_` cookie is set, or the token has expired, a new token will be gen
|
||||
This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases.
|
||||
:::
|
||||
|
||||
## Security Considerations
|
||||
|
||||
This middleware is designed to protect against CSRF attacks. It does not protect against other attack vectors, such as XSS, and should be used in combination with other security measures.
|
||||
|
||||
:::warning
|
||||
Never use 'safe' methods to mutate data. For example, never use a GET request to delete a resource. This middleware will not protect against CSRF attacks on 'safe' methods.
|
||||
:::
|
||||
|
||||
### The Double Submit Cookie Pattern (Default)
|
||||
|
||||
In the default configuration, the middleware will generate and store tokens using the `fiber.Storage` interface. These tokens are not associated with a user session, and, therefore, a Double Submit Cookie pattern is used to validate the token. This means that the token is stored in a cookie and also sent as a header on requests. The middleware will compare the cookie value with the header value to validate the token. This is a secure method of validating the token, as cookies are not accessible to JavaScript and, therefore, cannot be read by an attacker.
|
||||
|
||||
:::warning
|
||||
When using this method, it is important that you set the `CookieSameSite` option to `Lax` or `Strict` and that the Extractor is not `CsrfFromCookie`, and KeyLookup is not `cookie:<name>`.
|
||||
:::
|
||||
|
||||
### The Synchronizer Token Pattern (Session)
|
||||
|
||||
When using this middleware with a user session, the middleware can be configured to store the token in the session. This method is recommended when using a user session as it is generally more secure than the Double Submit Cookie Pattern.
|
||||
|
||||
:::warning
|
||||
When using this method, pre-sessions are required and will be created if a session is not already present. This means that the middleware will create a session for every safe request, even if the request does not require a session. Therefore it is required that the existence of a session is not used to indicate that a user is logged in or authenticated, and that a session value is used to indicate this instead.
|
||||
:::
|
||||
|
||||
### Defense In Depth
|
||||
|
||||
When using this middleware, it is recommended that you serve your pages over HTTPS, that the `CookieSecure` option is set to `true`, and that the `CookieSameSite` option is set to `Lax` or `Strict`. This will ensure that the cookie is only sent over HTTPS and that it is not sent on requests from external sites.
|
||||
|
||||
### Referer Checking
|
||||
|
||||
For HTTPS requests, this middleware performs strict referer checking. This means that even if a subdomain can set or modify cookies on your domain, it can’t force a user to post to your application since that request won’t come from your own exact domain.
|
||||
|
||||
### Token Lifecycle
|
||||
|
||||
Tokens are valid until they expire, or until they are deleted. By default, tokens are valid for 1 hour and each subsequent request will extend the expiration by 1 hour. This means that if a user makes a request every hour, the token will never expire. If a user makes a request after the token has expired, then a new token will be generated and the `csrf_` cookie will be set again. This means that the token will only expire if the user does not make a request for the duration of the expiration time.
|
||||
|
||||
#### Token Reuse
|
||||
|
||||
By default tokens may be used multiple times. This means that the token will not be deleted after it has been used. If you would like to delete the token after it has been used, then you can set the `SingleUseToken` option to `true`. This will delete the token after it has been used, and a new token will be generated on the next request.
|
||||
|
||||
:::note
|
||||
Using `SingleUseToken` comes with usability tradeoffs, and therefore is not enabled by default. It can interfere with the user experience if the user has multiple tabs open, or if the user uses the back button.
|
||||
:::
|
||||
|
||||
#### Deleting Tokens
|
||||
|
||||
When the authorization status changes, the CSRF token should be deleted and a new one generated. This can be done by calling `handler.DeleteToken(c)`. This will remove the token found in the request context from the storage and set the `csrf_` cookie to an empty value. The next 'safe' request will generate a new token and set the cookie again.
|
||||
|
||||
```go
|
||||
if handler, ok := app.AcquireCtx(ctx).Locals(ConfigDefault.HandlerContextKey).(*CSRFHandler); ok {
|
||||
if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil {
|
||||
// handle error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
If you are using this middleware with the fiber session middleware, then you can simply call `session.Destroy()`, `session.Regenerate()`, or `session.Reset()` to delete session and the token stored therein.
|
||||
:::
|
||||
|
||||
### BREACH
|
||||
|
||||
It is important to note that the token is sent as a header on every request, and if you include the token in a page that is vulnerable to [BREACH](https://en.wikipedia.org/wiki/BREACH), then an attacker may be able to extract the token. To mitigate this, you should take steps such as ensuring that your pages are served over HTTPS, that HTTP compression is disabled, and rate limiting requests.
|
||||
|
||||
## Signatures
|
||||
|
||||
```go
|
||||
@ -43,7 +107,7 @@ app.Use(csrf.New(csrf.Config{
|
||||
CookieName: "csrf_",
|
||||
CookieSameSite: "Lax",
|
||||
Expiration: 1 * time.Hour,
|
||||
KeyGenerator: utils.UUID,
|
||||
KeyGenerator: utils.UUIDv4,
|
||||
Extractor: func(c *fiber.Ctx) (string, error) { ... },
|
||||
}))
|
||||
```
|
||||
@ -52,6 +116,10 @@ app.Use(csrf.New(csrf.Config{
|
||||
KeyLookup will be ignored if Extractor is explicitly set.
|
||||
:::
|
||||
|
||||
### Use with fiber/middleware/session (recommended)
|
||||
|
||||
It's recommended to use this middleware with [fiber/middleware/session](https://docs.gofiber.io/api/middleware/session) to store the CSRF token in the session. This is generally more secure than the default configuration.
|
||||
|
||||
## Config
|
||||
|
||||
### Config
|
||||
@ -60,7 +128,7 @@ KeyLookup will be ignored if Extractor is explicitly set.
|
||||
|:------------------|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------|
|
||||
| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
|
||||
| KeyLookup | `string` | 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. | "header:X-CSRF-Token" |
|
||||
| CookieName | `string` | Name of the session cookie. This cookie will store the session key. | "csrf_" |
|
||||
| CookieName | `string` | Name of the csrf cookie. This cookie will store the csrf key. | "csrf_" |
|
||||
| CookieDomain | `string` | Domain of the CSRF cookie. | "" |
|
||||
| CookiePath | `string` | Path of the CSRF cookie. | "" |
|
||||
| CookieSecure | `bool` | Indicates if the CSRF cookie is secure. | false |
|
||||
@ -68,26 +136,51 @@ KeyLookup will be ignored if Extractor is explicitly set.
|
||||
| CookieSameSite | `string` | Value of SameSite cookie. | "Lax" |
|
||||
| CookieSessionOnly | `bool` | Decides whether the cookie should last for only the browser session. Ignores Expiration if set to true. | false |
|
||||
| Expiration | `time.Duration` | Expiration is the duration before the CSRF token will expire. | 1 * time.Hour |
|
||||
| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | memory.New() |
|
||||
| SingleUseToken | `bool` | SingleUseToken indicates if the CSRF token be destroyed and a new one generated on each use. (See TokenLifecycle) | false |
|
||||
| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | `nil` |
|
||||
| Session | `*session.Store` | Session is used to store the state of the middleware. Overrides Storage if set. | `nil` |
|
||||
| SessionKey | `string` | SessionKey is the key used to store the token in the session. | "fiber.csrf.token" |
|
||||
| ContextKey | `string` | Context key to store the generated CSRF token into the context. If left empty, the token will not be stored in the context. | "" |
|
||||
| KeyGenerator | `func() string` | KeyGenerator creates a new CSRF token. | utils.UUID |
|
||||
| CookieExpires | `time.Duration` (Deprecated) | Deprecated: Please use Expiration. | 0 |
|
||||
| Cookie | `*fiber.Cookie` (Deprecated) | Deprecated: Please use Cookie* related fields. | nil |
|
||||
| Cookie | `*fiber.Cookie` (Deprecated) | Deprecated: Please use Cookie* related fields. | `nil` |
|
||||
| TokenLookup | `string` (Deprecated) | Deprecated: Please use KeyLookup. | "" |
|
||||
| ErrorHandler | `fiber.ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. | DefaultErrorHandler |
|
||||
| Extractor | `func(*fiber.Ctx) (string, error)` | Extractor returns the CSRF token. If set, this will be used in place of an Extractor based on KeyLookup. | Extractor based on KeyLookup |
|
||||
| HandlerContextKey | `string` | HandlerContextKey is used to store the CSRF Handler into context. | "fiber.csrf.handler" |
|
||||
|
||||
## Default Config
|
||||
|
||||
```go
|
||||
var ConfigDefault = Config{
|
||||
KeyLookup: "header:" + HeaderName,
|
||||
CookieName: "csrf_",
|
||||
CookieSameSite: "Lax",
|
||||
Expiration: 1 * time.Hour,
|
||||
KeyGenerator: utils.UUID,
|
||||
ErrorHandler: defaultErrorHandler,
|
||||
Extractor: CsrfFromHeader(HeaderName),
|
||||
KeyLookup: "header:" + HeaderName,
|
||||
CookieName: "csrf_",
|
||||
CookieSameSite: "Lax",
|
||||
Expiration: 1 * time.Hour,
|
||||
KeyGenerator: utils.UUIDv4,
|
||||
ErrorHandler: defaultErrorHandler,
|
||||
Extractor: CsrfFromHeader(HeaderName),
|
||||
SessionKey: "fiber.csrf.token",
|
||||
HandlerContextKey: "fiber.csrf.handler",
|
||||
}
|
||||
```
|
||||
|
||||
## Recommended Config (with session)
|
||||
|
||||
```go
|
||||
var ConfigDefault = Config{
|
||||
KeyLookup: "header:" + HeaderName,
|
||||
CookieName: "csrf_",
|
||||
CookieSameSite: "Lax",
|
||||
CookieSessionOnly: true,
|
||||
CookieHTTPOnly: true,
|
||||
Expiration: 1 * time.Hour,
|
||||
KeyGenerator: utils.UUIDv4,
|
||||
ErrorHandler: defaultErrorHandler,
|
||||
Extractor: CsrfFromHeader(HeaderName),
|
||||
Session: session.Store,
|
||||
SessionKey: "fiber.csrf.token",
|
||||
HandlerContextKey: "fiber.csrf.handler",
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/gofiber/fiber/v2/middleware/session"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
)
|
||||
|
||||
@ -33,6 +34,7 @@ type Config struct {
|
||||
|
||||
// Name of the session cookie. This cookie will store session key.
|
||||
// Optional. Default value "csrf_".
|
||||
// Overriden if KeyLookup == "cookie:<name>"
|
||||
CookieName string
|
||||
|
||||
// Domain of the CSRF cookie.
|
||||
@ -64,11 +66,29 @@ type Config struct {
|
||||
// Optional. Default: 1 * time.Hour
|
||||
Expiration time.Duration
|
||||
|
||||
// SingleUseToken indicates if the CSRF token be destroyed
|
||||
// and a new one generated on each use.
|
||||
//
|
||||
// Optional. Default: false
|
||||
SingleUseToken bool
|
||||
|
||||
// Store is used to store the state of the middleware
|
||||
//
|
||||
// Optional. Default: memory.New()
|
||||
// Ignored if Session is set.
|
||||
Storage fiber.Storage
|
||||
|
||||
// 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
|
||||
Session *session.Store
|
||||
|
||||
// SessionKey is the key used to store the token in the session
|
||||
//
|
||||
// Default: "fiber.csrf.token"
|
||||
SessionKey string
|
||||
|
||||
// Context key to store generated CSRF token into context.
|
||||
// If left empty, token will not be stored in context.
|
||||
//
|
||||
@ -100,19 +120,26 @@ type Config struct {
|
||||
//
|
||||
// Optional. Default will create an Extractor based on KeyLookup.
|
||||
Extractor func(c *fiber.Ctx) (string, error)
|
||||
|
||||
// HandlerContextKey is used to store the CSRF Handler into context
|
||||
//
|
||||
// Default: "fiber.csrf.handler"
|
||||
HandlerContextKey string
|
||||
}
|
||||
|
||||
const HeaderName = "X-Csrf-Token"
|
||||
|
||||
// ConfigDefault is the default config
|
||||
var ConfigDefault = Config{
|
||||
KeyLookup: "header:" + HeaderName,
|
||||
CookieName: "csrf_",
|
||||
CookieSameSite: "Lax",
|
||||
Expiration: 1 * time.Hour,
|
||||
KeyGenerator: utils.UUID,
|
||||
ErrorHandler: defaultErrorHandler,
|
||||
Extractor: CsrfFromHeader(HeaderName),
|
||||
KeyLookup: "header:" + HeaderName,
|
||||
CookieName: "csrf_",
|
||||
CookieSameSite: "Lax",
|
||||
Expiration: 1 * time.Hour,
|
||||
KeyGenerator: utils.UUIDv4,
|
||||
ErrorHandler: defaultErrorHandler,
|
||||
Extractor: CsrfFromHeader(HeaderName),
|
||||
SessionKey: "fiber.csrf.token",
|
||||
HandlerContextKey: "fiber.csrf.handler",
|
||||
}
|
||||
|
||||
// default ErrorHandler that process return error from fiber.Handler
|
||||
@ -174,6 +201,12 @@ func configDefault(config ...Config) Config {
|
||||
if cfg.ErrorHandler == nil {
|
||||
cfg.ErrorHandler = ConfigDefault.ErrorHandler
|
||||
}
|
||||
if cfg.SessionKey == "" {
|
||||
cfg.SessionKey = ConfigDefault.SessionKey
|
||||
}
|
||||
if cfg.HandlerContextKey == "" {
|
||||
cfg.HandlerContextKey = ConfigDefault.HandlerContextKey
|
||||
}
|
||||
|
||||
// Generate the correct extractor to get the token from the correct location
|
||||
selectors := strings.Split(cfg.KeyLookup, ":")
|
||||
@ -195,7 +228,14 @@ func configDefault(config ...Config) Config {
|
||||
case "param":
|
||||
cfg.Extractor = CsrfFromParam(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 = CsrfFromCookie(selectors[1])
|
||||
cfg.CookieName = selectors[1] // Cookie name is the same as the key
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,22 +2,42 @@ package csrf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
var errTokenNotFound = errors.New("csrf token not found")
|
||||
var (
|
||||
ErrTokenNotFound = errors.New("csrf token not found")
|
||||
ErrTokenInvalid = errors.New("csrf token invalid")
|
||||
ErrNoReferer = errors.New("referer not supplied")
|
||||
ErrBadReferer = errors.New("referer invalid")
|
||||
dummyValue = []byte{'+'}
|
||||
)
|
||||
|
||||
type CSRFHandler struct {
|
||||
config *Config
|
||||
sessionManager *sessionManager
|
||||
storageManager *storageManager
|
||||
}
|
||||
|
||||
// New creates a new middleware handler
|
||||
func New(config ...Config) fiber.Handler {
|
||||
// Set default config
|
||||
cfg := configDefault(config...)
|
||||
|
||||
// Create manager to simplify storage operations ( see manager.go )
|
||||
manager := newManager(cfg.Storage)
|
||||
// Create manager to simplify storage operations ( see *_manager.go )
|
||||
var sessionManager *sessionManager
|
||||
var storageManager *storageManager
|
||||
if cfg.Session != nil {
|
||||
// Register the Token struct in the session store
|
||||
cfg.Session.RegisterType(Token{})
|
||||
|
||||
dummyValue := []byte{'+'}
|
||||
sessionManager = newSessionManager(cfg.Session, cfg.SessionKey)
|
||||
} else {
|
||||
storageManager = newStorageManager(cfg.Storage)
|
||||
}
|
||||
|
||||
// Return new handler
|
||||
return func(c *fiber.Ctx) error {
|
||||
@ -26,36 +46,69 @@ func New(config ...Config) fiber.Handler {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Store the CSRF handler in the context if a context key is specified
|
||||
if cfg.HandlerContextKey != "" {
|
||||
c.Locals(cfg.HandlerContextKey, &CSRFHandler{
|
||||
config: &cfg,
|
||||
sessionManager: sessionManager,
|
||||
storageManager: storageManager,
|
||||
})
|
||||
}
|
||||
|
||||
var token string
|
||||
|
||||
// Action depends on the HTTP method
|
||||
switch c.Method() {
|
||||
case fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace:
|
||||
// Declare empty token and try to get existing CSRF from cookie
|
||||
token = c.Cookies(cfg.CookieName)
|
||||
cookieToken := c.Cookies(cfg.CookieName)
|
||||
|
||||
if cookieToken != "" {
|
||||
rawToken := getTokenFromStorage(c, cookieToken, cfg, sessionManager, storageManager)
|
||||
|
||||
if rawToken != nil {
|
||||
token = string(rawToken)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Assume that anything not defined as 'safe' by RFC7231 needs protection
|
||||
|
||||
// Enforce an origin check for HTTPS connections.
|
||||
if c.Protocol() == "https" {
|
||||
if err := refererMatchesHost(c); err != nil {
|
||||
return cfg.ErrorHandler(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract token from client request i.e. header, query, param, form or cookie
|
||||
token, err := cfg.Extractor(c)
|
||||
extractedToken, err := cfg.Extractor(c)
|
||||
if err != nil {
|
||||
return cfg.ErrorHandler(c, err)
|
||||
}
|
||||
|
||||
// if token does not exist in Storage
|
||||
if manager.getRaw(token) == nil {
|
||||
// Expire cookie
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: cfg.CookieName,
|
||||
Domain: cfg.CookieDomain,
|
||||
Path: cfg.CookiePath,
|
||||
Expires: time.Now().Add(-1 * time.Minute),
|
||||
Secure: cfg.CookieSecure,
|
||||
HTTPOnly: cfg.CookieHTTPOnly,
|
||||
SameSite: cfg.CookieSameSite,
|
||||
SessionOnly: cfg.CookieSessionOnly,
|
||||
})
|
||||
return cfg.ErrorHandler(c, errTokenNotFound)
|
||||
if extractedToken == "" {
|
||||
return cfg.ErrorHandler(c, ErrTokenNotFound)
|
||||
}
|
||||
|
||||
// If not using CsrfFromCookie 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 !isCsrfFromCookie(cfg.Extractor) && extractedToken != c.Cookies(cfg.CookieName) {
|
||||
return cfg.ErrorHandler(c, ErrTokenInvalid)
|
||||
}
|
||||
|
||||
rawToken := getTokenFromStorage(c, extractedToken, cfg, sessionManager, storageManager)
|
||||
|
||||
if rawToken == nil {
|
||||
// If token is not in storage, expire the cookie
|
||||
expireCSRFCookie(c, cfg)
|
||||
// and return an error
|
||||
return cfg.ErrorHandler(c, ErrTokenNotFound)
|
||||
}
|
||||
if cfg.SingleUseToken {
|
||||
// If token is single use, delete it from storage
|
||||
deleteTokenFromStorage(c, extractedToken, cfg, sessionManager, storageManager)
|
||||
} else {
|
||||
token = string(rawToken)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,29 +118,16 @@ func New(config ...Config) fiber.Handler {
|
||||
token = cfg.KeyGenerator()
|
||||
}
|
||||
|
||||
// Add/update token to Storage
|
||||
manager.setRaw(token, dummyValue, cfg.Expiration)
|
||||
// Create or extend the token in the storage
|
||||
createOrExtendTokenInStorage(c, token, cfg, sessionManager, storageManager)
|
||||
|
||||
// Create cookie to pass token to client
|
||||
cookie := &fiber.Cookie{
|
||||
Name: cfg.CookieName,
|
||||
Value: token,
|
||||
Domain: cfg.CookieDomain,
|
||||
Path: cfg.CookiePath,
|
||||
Expires: time.Now().Add(cfg.Expiration),
|
||||
Secure: cfg.CookieSecure,
|
||||
HTTPOnly: cfg.CookieHTTPOnly,
|
||||
SameSite: cfg.CookieSameSite,
|
||||
SessionOnly: cfg.CookieSessionOnly,
|
||||
}
|
||||
// Set cookie to response
|
||||
c.Cookie(cookie)
|
||||
// Update the CSRF cookie
|
||||
updateCSRFCookie(c, cfg, token)
|
||||
|
||||
// Protect clients from caching the response by telling the browser
|
||||
// a new header value is generated
|
||||
// Tell the browser that a new header value is generated
|
||||
c.Vary(fiber.HeaderCookie)
|
||||
|
||||
// Store token in context if set
|
||||
// Store the token in the context if a context key is specified
|
||||
if cfg.ContextKey != "" {
|
||||
c.Locals(cfg.ContextKey, token)
|
||||
}
|
||||
@ -96,3 +136,95 @@ func New(config ...Config) fiber.Handler {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// getTokenFromStorage returns the raw token from the storage
|
||||
// returns nil if the token does not exist, is expired or is invalid
|
||||
func getTokenFromStorage(c *fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) []byte {
|
||||
if cfg.Session != nil {
|
||||
return sessionManager.getRaw(c, token, dummyValue)
|
||||
}
|
||||
return storageManager.getRaw(token)
|
||||
}
|
||||
|
||||
// createOrExtendTokenInStorage creates or extends the token in the storage
|
||||
func createOrExtendTokenInStorage(c *fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) {
|
||||
if cfg.Session != nil {
|
||||
sessionManager.setRaw(c, token, dummyValue, cfg.Expiration)
|
||||
} else {
|
||||
storageManager.setRaw(token, dummyValue, cfg.Expiration)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTokenFromStorage(c *fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) {
|
||||
if cfg.Session != nil {
|
||||
sessionManager.delRaw(c)
|
||||
} else {
|
||||
storageManager.delRaw(token)
|
||||
}
|
||||
}
|
||||
|
||||
// Update CSRF cookie
|
||||
// if expireCookie is true, the cookie will expire immediately
|
||||
func updateCSRFCookie(c *fiber.Ctx, cfg Config, token string) {
|
||||
setCSRFCookie(c, cfg, token, cfg.Expiration)
|
||||
}
|
||||
|
||||
func expireCSRFCookie(c *fiber.Ctx, cfg Config) {
|
||||
setCSRFCookie(c, cfg, "", -time.Hour)
|
||||
}
|
||||
|
||||
func setCSRFCookie(c *fiber.Ctx, cfg Config, token string, expiry time.Duration) {
|
||||
cookie := &fiber.Cookie{
|
||||
Name: cfg.CookieName,
|
||||
Value: token,
|
||||
Domain: cfg.CookieDomain,
|
||||
Path: cfg.CookiePath,
|
||||
Secure: cfg.CookieSecure,
|
||||
HTTPOnly: cfg.CookieHTTPOnly,
|
||||
SameSite: cfg.CookieSameSite,
|
||||
SessionOnly: cfg.CookieSessionOnly,
|
||||
Expires: time.Now().Add(expiry),
|
||||
}
|
||||
|
||||
// Set the CSRF cookie to the response
|
||||
c.Cookie(cookie)
|
||||
}
|
||||
|
||||
// DeleteToken removes the token found in the context from the storage
|
||||
// and expires the CSRF cookie
|
||||
func (handler *CSRFHandler) DeleteToken(c *fiber.Ctx) error {
|
||||
// Get the config from the context
|
||||
config := handler.config
|
||||
if config == nil {
|
||||
panic("CSRFHandler config not found in context")
|
||||
}
|
||||
// Extract token from the client request cookie
|
||||
cookieToken := c.Cookies(config.CookieName)
|
||||
if cookieToken == "" {
|
||||
return config.ErrorHandler(c, ErrTokenNotFound)
|
||||
}
|
||||
// Remove the token from storage
|
||||
deleteTokenFromStorage(c, cookieToken, *config, handler.sessionManager, handler.storageManager)
|
||||
// Expire the cookie
|
||||
expireCSRFCookie(c, *config)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isCsrfFromCookie checks if the extractor is set to ExtractFromCookie
|
||||
func isCsrfFromCookie(extractor interface{}) bool {
|
||||
return reflect.ValueOf(extractor).Pointer() == reflect.ValueOf(CsrfFromCookie).Pointer()
|
||||
}
|
||||
|
||||
// refererMatchesHost checks that the referer header matches the host header
|
||||
// returns an error if the referer header is not present or is invalid
|
||||
// returns nil if the referer header is valid
|
||||
func refererMatchesHost(c *fiber.Ctx) error {
|
||||
referer := c.Get(fiber.HeaderReferer)
|
||||
if referer == "" {
|
||||
return ErrNoReferer
|
||||
}
|
||||
if referer != c.Protocol()+"://"+c.Hostname() {
|
||||
return ErrBadReferer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/session"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
@ -58,11 +59,146 @@ func Test_CSRF(t *testing.T) {
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CSRF_WithSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// session store
|
||||
store := session.New(session.Config{
|
||||
KeyLookup: "cookie:_session",
|
||||
})
|
||||
|
||||
// fiber instance
|
||||
app := fiber.New()
|
||||
|
||||
// fiber context
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
defer app.ReleaseCtx(app.AcquireCtx(ctx))
|
||||
|
||||
// get session
|
||||
sess, err := store.Get(app.AcquireCtx(ctx))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, sess.Fresh())
|
||||
|
||||
// the session string is no longer be 123
|
||||
newSessionIDString := sess.ID()
|
||||
app.AcquireCtx(ctx).Request().Header.SetCookie("_session", newSessionIDString)
|
||||
|
||||
// middleware config
|
||||
config := Config{
|
||||
Session: store,
|
||||
}
|
||||
|
||||
// middleware
|
||||
app.Use(New(config))
|
||||
|
||||
app.Post("/", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
|
||||
methods := [4]string{fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace}
|
||||
|
||||
for _, method := range methods {
|
||||
// Generate CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Header.SetCookie("_session", newSessionIDString)
|
||||
h(ctx)
|
||||
|
||||
// Without CSRF cookie
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.SetCookie("_session", newSessionIDString)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
|
||||
// Empty/invalid CSRF token
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, "johndoe")
|
||||
ctx.Request.Header.SetCookie("_session", newSessionIDString)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
|
||||
// Valid CSRF token
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(method)
|
||||
ctx.Request.Header.SetCookie("_session", newSessionIDString)
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
for _, header := range strings.Split(token, ";") {
|
||||
if strings.Split(strings.TrimSpace(header), "=")[0] == ConfigDefault.CookieName {
|
||||
token = strings.Split(header, "=")[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie("_session", newSessionIDString)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
}
|
||||
}
|
||||
|
||||
// go test -run Test_CSRF_SingleUseToken
|
||||
func Test_CSRF_SingleUseToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(New(Config{
|
||||
SingleUseToken: true,
|
||||
}))
|
||||
|
||||
app.Post("/", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
|
||||
// Generate CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
|
||||
|
||||
// Use the CSRF token
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
newToken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
newToken = strings.Split(strings.Split(newToken, ";")[0], "=")[1]
|
||||
if token == newToken {
|
||||
t.Error("new token should not be the same as the old token")
|
||||
}
|
||||
|
||||
// Use the CSRF token again
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
// go test -run Test_CSRF_Next
|
||||
func Test_CSRF_Next(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -127,6 +263,7 @@ func Test_CSRF_From_Form(t *testing.T) {
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)
|
||||
ctx.Request.SetBodyString("_csrf=" + token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
}
|
||||
@ -146,7 +283,7 @@ func Test_CSRF_From_Query(t *testing.T) {
|
||||
|
||||
// Invalid CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.SetRequestURI("/?_csrf=" + utils.UUID())
|
||||
ctx.Request.SetRequestURI("/?_csrf=" + utils.UUIDv4())
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
|
||||
@ -163,6 +300,7 @@ func Test_CSRF_From_Query(t *testing.T) {
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.SetRequestURI("/?_csrf=" + token)
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
utils.AssertEqual(t, "OK", string(ctx.Response.Body()))
|
||||
@ -183,7 +321,7 @@ func Test_CSRF_From_Param(t *testing.T) {
|
||||
|
||||
// Invalid CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.SetRequestURI("/" + utils.UUID())
|
||||
ctx.Request.SetRequestURI("/" + utils.UUIDv4())
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
|
||||
@ -191,7 +329,7 @@ func Test_CSRF_From_Param(t *testing.T) {
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.SetRequestURI("/" + utils.UUID())
|
||||
ctx.Request.SetRequestURI("/" + utils.UUIDv4())
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
|
||||
@ -200,6 +338,7 @@ func Test_CSRF_From_Param(t *testing.T) {
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.SetRequestURI("/" + token)
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
utils.AssertEqual(t, "OK", string(ctx.Response.Body()))
|
||||
@ -221,7 +360,7 @@ func Test_CSRF_From_Cookie(t *testing.T) {
|
||||
// Invalid CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.SetRequestURI("/")
|
||||
ctx.Request.Header.Set(fiber.HeaderCookie, "csrf="+utils.UUID()+";")
|
||||
ctx.Request.Header.Set(fiber.HeaderCookie, "csrf="+utils.UUIDv4()+";")
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
|
||||
@ -285,16 +424,170 @@ func Test_CSRF_From_Custom(t *testing.T) {
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain)
|
||||
ctx.Request.SetBodyString("_csrf=" + token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func Test_CSRF_Referer(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(New(Config{CookieSecure: true}))
|
||||
|
||||
app.Post("/", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
|
||||
|
||||
// Test Correct Referer
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com")
|
||||
ctx.Request.Header.Set(fiber.HeaderReferer, "https://example.com")
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 200, ctx.Response.StatusCode())
|
||||
|
||||
// Test Wrong Referer
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com")
|
||||
ctx.Request.Header.Set(fiber.HeaderReferer, "https://csrf.example.com")
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func Test_CSRF_DeleteToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
config := ConfigDefault
|
||||
|
||||
app.Use(New(config))
|
||||
|
||||
app.Post("/", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
|
||||
// Generate CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
|
||||
|
||||
// Delete the CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
if handler, ok := app.AcquireCtx(ctx).Locals(ConfigDefault.HandlerContextKey).(*CSRFHandler); ok {
|
||||
if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
h(ctx)
|
||||
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func Test_CSRF_DeleteToken_WithSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// session store
|
||||
store := session.New(session.Config{
|
||||
KeyLookup: "cookie:_session",
|
||||
})
|
||||
|
||||
// fiber instance
|
||||
app := fiber.New()
|
||||
|
||||
// fiber context
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
defer app.ReleaseCtx(app.AcquireCtx(ctx))
|
||||
|
||||
// get session
|
||||
sess, err := store.Get(app.AcquireCtx(ctx))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, sess.Fresh())
|
||||
|
||||
// the session string is no longer be 123
|
||||
newSessionIDString := sess.ID()
|
||||
app.AcquireCtx(ctx).Request().Header.SetCookie("_session", newSessionIDString)
|
||||
|
||||
// middleware config
|
||||
config := Config{
|
||||
Session: store,
|
||||
}
|
||||
|
||||
// middleware
|
||||
app.Use(New(config))
|
||||
|
||||
app.Post("/", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
|
||||
// Generate CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Header.SetCookie("_session", newSessionIDString)
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
|
||||
|
||||
// Delete the CSRF token
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
if handler, ok := app.AcquireCtx(ctx).Locals(ConfigDefault.HandlerContextKey).(*CSRFHandler); ok {
|
||||
if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
h(ctx)
|
||||
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
ctx.Request.Header.SetCookie("_session", newSessionIDString)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func Test_CSRF_ErrorHandler_InvalidToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
errHandler := func(ctx *fiber.Ctx, err error) error {
|
||||
utils.AssertEqual(t, errTokenNotFound, err)
|
||||
utils.AssertEqual(t, ErrTokenInvalid, err)
|
||||
return ctx.Status(419).Send([]byte("invalid CSRF token"))
|
||||
}
|
||||
|
||||
@ -352,6 +645,74 @@ func Test_CSRF_ErrorHandler_EmptyToken(t *testing.T) {
|
||||
utils.AssertEqual(t, "empty CSRF token", string(ctx.Response.Body()))
|
||||
}
|
||||
|
||||
func Test_CSRF_ErrorHandler_MissingReferer(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
errHandler := func(ctx *fiber.Ctx, err error) error {
|
||||
utils.AssertEqual(t, ErrNoReferer, err)
|
||||
return ctx.Status(419).Send([]byte("empty CSRF token"))
|
||||
}
|
||||
|
||||
app.Use(New(Config{
|
||||
CookieSecure: true,
|
||||
ErrorHandler: errHandler,
|
||||
}))
|
||||
|
||||
app.Post("/", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
|
||||
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||
ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com")
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 419, ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func Test_CSRF_Cookie_Injection_Exploit(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(New())
|
||||
|
||||
app.Post("/", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
|
||||
// Inject CSRF token
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
ctx.Request.Header.Set(fiber.HeaderCookie, "csrf_=pwned;")
|
||||
ctx.Request.SetRequestURI("/")
|
||||
h(ctx)
|
||||
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
|
||||
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
|
||||
|
||||
// Exploit CSRF token we just injected
|
||||
ctx.Request.Reset()
|
||||
ctx.Response.Reset()
|
||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||
ctx.Request.Header.Set(HeaderName, token)
|
||||
ctx.Request.Header.Set(fiber.HeaderCookie, "csrf_=pwned;")
|
||||
h(ctx)
|
||||
utils.AssertEqual(t, 403, ctx.Response.StatusCode(), "CSRF exploit successful")
|
||||
}
|
||||
|
||||
// TODO: use this test case and make the unsafe header value bug from https://github.com/gofiber/fiber/issues/2045 reproducible and permanently fixed/tested by this testcase
|
||||
// func Test_CSRF_UnsafeHeaderValue(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
7
middleware/csrf/helpers.go
Normal file
7
middleware/csrf/helpers.go
Normal file
@ -0,0 +1,7 @@
|
||||
package csrf
|
||||
|
||||
import "crypto/subtle"
|
||||
|
||||
func compareTokens(a, b []byte) bool {
|
||||
return subtle.ConstantTimeCompare(a, b) == 1
|
||||
}
|
68
middleware/csrf/session_manager.go
Normal file
68
middleware/csrf/session_manager.go
Normal file
@ -0,0 +1,68 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/gofiber/fiber/v2/middleware/session"
|
||||
)
|
||||
|
||||
type sessionManager struct {
|
||||
key string
|
||||
session *session.Store
|
||||
}
|
||||
|
||||
func newSessionManager(s *session.Store, k string) *sessionManager {
|
||||
// Create new storage handler
|
||||
sessionManager := &sessionManager{
|
||||
key: k,
|
||||
}
|
||||
if s != nil {
|
||||
// Use provided storage if provided
|
||||
sessionManager.session = s
|
||||
}
|
||||
return sessionManager
|
||||
}
|
||||
|
||||
// get token from session
|
||||
func (m *sessionManager) getRaw(c *fiber.Ctx, key string, raw []byte) []byte {
|
||||
sess, err := m.session.Get(c)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
token, ok := sess.Get(m.key).(Token)
|
||||
if ok {
|
||||
if token.Expiration.Before(time.Now()) || key != token.Key || !compareTokens(raw, token.Raw) {
|
||||
return nil
|
||||
}
|
||||
return token.Raw
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// set token in session
|
||||
func (m *sessionManager) setRaw(c *fiber.Ctx, key string, raw []byte, exp time.Duration) {
|
||||
sess, err := m.session.Get(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// the key is crucial in crsf and sometimes a reference to another value which can be reused later(pool/unsafe values concept), so a copy is made here
|
||||
sess.Set(m.key, &Token{key, raw, time.Now().Add(exp)})
|
||||
if err := sess.Save(); err != nil {
|
||||
log.Warn("csrf: failed to save session: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delete token from session
|
||||
func (m *sessionManager) delRaw(c *fiber.Ctx) {
|
||||
sess, err := m.session.Get(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sess.Delete(m.key)
|
||||
if err := sess.Save(); err != nil {
|
||||
log.Warn("csrf: failed to save session: ", err)
|
||||
}
|
||||
}
|
@ -10,19 +10,19 @@ import (
|
||||
)
|
||||
|
||||
// go:generate msgp
|
||||
// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported
|
||||
// msgp -file="storage_manager.go" -o="storage_manager_msgp.go" -tests=false -unexported
|
||||
type item struct{}
|
||||
|
||||
//msgp:ignore manager
|
||||
type manager struct {
|
||||
type storageManager struct {
|
||||
pool sync.Pool
|
||||
memory *memory.Storage
|
||||
storage fiber.Storage
|
||||
}
|
||||
|
||||
func newManager(storage fiber.Storage) *manager {
|
||||
func newStorageManager(storage fiber.Storage) *storageManager {
|
||||
// Create new storage handler
|
||||
manager := &manager{
|
||||
storageManager := &storageManager{
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(item)
|
||||
@ -31,16 +31,16 @@ func newManager(storage fiber.Storage) *manager {
|
||||
}
|
||||
if storage != nil {
|
||||
// Use provided storage if provided
|
||||
manager.storage = storage
|
||||
storageManager.storage = storage
|
||||
} else {
|
||||
// Fallback too memory storage
|
||||
manager.memory = memory.New()
|
||||
storageManager.memory = memory.New()
|
||||
}
|
||||
return manager
|
||||
return storageManager
|
||||
}
|
||||
|
||||
// get raw data from storage or memory
|
||||
func (m *manager) getRaw(key string) []byte {
|
||||
func (m *storageManager) getRaw(key string) []byte {
|
||||
var raw []byte
|
||||
if m.storage != nil {
|
||||
raw, _ = m.storage.Get(key) //nolint:errcheck // TODO: Do not ignore error
|
||||
@ -51,7 +51,7 @@ func (m *manager) getRaw(key string) []byte {
|
||||
}
|
||||
|
||||
// set data to storage or memory
|
||||
func (m *manager) setRaw(key string, raw []byte, exp time.Duration) {
|
||||
func (m *storageManager) setRaw(key string, raw []byte, exp time.Duration) {
|
||||
if m.storage != nil {
|
||||
_ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Do not ignore error
|
||||
} else {
|
||||
@ -59,3 +59,12 @@ func (m *manager) setRaw(key string, raw []byte, exp time.Duration) {
|
||||
m.memory.Set(utils.CopyString(key), raw, exp)
|
||||
}
|
||||
}
|
||||
|
||||
// delete data from storage or memory
|
||||
func (m *storageManager) delRaw(key string) {
|
||||
if m.storage != nil {
|
||||
_ = m.storage.Delete(key) //nolint:errcheck // TODO: Do not ignore error
|
||||
} else {
|
||||
m.memory.Delete(key)
|
||||
}
|
||||
}
|
9
middleware/csrf/token.go
Normal file
9
middleware/csrf/token.go
Normal file
@ -0,0 +1,9 @@
|
||||
package csrf
|
||||
|
||||
import "time"
|
||||
|
||||
type Token struct {
|
||||
Key string `json:"key"`
|
||||
Raw []byte `json:"raw"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user