1
0
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:
Jason McNeil 2023-10-11 09:41:42 -03:00 committed by GitHub
parent 9292a36e28
commit b50d91d58e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 792 additions and 73 deletions

View File

@ -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 cant force a user to post to your application since that request wont 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",
}
```

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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()

View File

@ -0,0 +1,7 @@
package csrf
import "crypto/subtle"
func compareTokens(a, b []byte) bool {
return subtle.ConstantTimeCompare(a, b) == 1
}

View 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)
}
}

View File

@ -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
View 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"`
}