feat(middleware/csrf): Add support for trusted origins (#2910)

* feat(middleware/csrf): Add support for trusted origins in CSRF middleware

* fix(middleware/csrf): lint errors

* docs(middleware/csrf): following the ai

* fix(middleware/csrf): isSameSchemeAndDomain

* fix(middleware/csrf): null origin

expand tests to check invalid urls in headers

* chore(middleware/csrf): Sentinel Errors

test(middleware/csrf): improve coverage

* docs: add extra space between sentences.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore(middleware/csrf): remove trailing newline in csrf_test.go

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
pull/2906/head^2
Jason McNeil 2024-03-10 13:35:55 -03:00 committed by GitHub
parent 7bc43dcabf
commit fcb853788b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 654 additions and 156 deletions

View File

@ -10,107 +10,7 @@ This middleware offers two [Token Validation Patterns](#token-validation-pattern
As a [Defense In Depth](#defense-in-depth) measure, this middleware performs [Referer Checking](#referer-checking) for HTTPS requests.
## Token Generation
CSRF tokens are generated on 'safe' requests and when the existing token has expired or hasn't been set yet. If `SingleUseToken` is `true`, a new token is generated after each use. Retrieve the CSRF token using `csrf.TokenFromContext(c)`.
## Security Considerations
This middleware is designed to protect against CSRF attacks but does not protect against other attack vectors, such as XSS. It should be used in combination with other security measures.
:::danger
Never use 'safe' methods to mutate data, for example, never use a GET request to modify a resource. This middleware will not protect against CSRF attacks on 'safe' methods.
:::
### Token Validation Patterns
#### Double Submit Cookie Pattern (Default)
By default, the middleware generates and stores tokens using the `fiber.Storage` interface. These tokens are not linked to any particular user session, and they are validated using the Double Submit Cookie pattern. The token is stored in a cookie, and then sent as a header on requests. The middleware compares the cookie value with the header value to validate the token. This is a secure pattern that does not require a user session.
When the authorization status changes, the previously issued token MUST be deleted, and a new one generated. See [Token Lifecycle](#token-lifecycle) [Deleting Tokens](#deleting-tokens) for more information.
:::caution
When using this pattern, it's important to set the `CookieSameSite` option to `Lax` or `Strict` and ensure that the Extractor is not `FromCookie`, and KeyLookup is not `cookie:<name>`.
:::
:::note
When using this pattern, this middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for Storage saves data to memory. See [Custom Storage/Database](#custom-storagedatabase) for customizing the storage.
:::
#### Synchronizer Token Pattern (with Session)
When using this middleware with a user session, the middleware can be configured to store the token within the session. This method is recommended when using a user session, as it is generally more secure than the Double Submit Cookie Pattern.
When using this pattern it's important to regenerate the session when the authorization status changes, this will also delete the token. See: [Token Lifecycle](#token-lifecycle) for more information.
:::caution
Pre-sessions are required and will be created automatically if not present. Use a session value to indicate authentication instead of relying on presence of a session.
:::
### Defense In Depth
When using this middleware, it's recommended to serve your pages over HTTPS, set the `CookieSecure` option to `true`, and set the `CookieSameSite` option to `Lax` or `Strict`. This ensures that the cookie is only sent over HTTPS and not on requests from external sites.
:::note
Cookie prefixes `__Host-` and `__Secure-` can be used to further secure the cookie. Note that these prefixes are not supported by all browsers and there are other limitations. See [MDN#Set-Cookie#cookie_prefixes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes) for more information.
To use these prefixes, set the `CookieName` option to `__Host-csrf_` or `__Secure-csrf_`.
:::
### Referer Checking
For HTTPS requests, this middleware performs strict referer checking. 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.
:::caution
When HTTPS requests are protected by CSRF, referer checking is always carried out.
The Referer header is automatically included in requests by all modern browsers, including those made using the JS Fetch API. However, if you're making use of this middleware with a custom client, it's important to ensure that the client sends a valid Referer header.
:::
### 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 extends the expiration by 1 hour. The token only expires if the user doesn't make a request for the duration of the expiration time.
#### Token Reuse
By default, tokens may be used multiple times. If you want to delete the token after it has been used, 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.
:::info
Using `SingleUseToken` comes with usability trade-offs and is not enabled by default. For example, it can interfere with the user experience if the user has multiple tabs open or uses the back button.
:::
#### Deleting Tokens
When the authorization status changes, the CSRF token MUST be deleted, and a new one generated. This can be done by calling `handler.DeleteToken(c)`.
```go
handler := csrf.HandlerFromContext(ctx)
if handler != nil {
if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil {
// handle error
}
}
```
:::tip
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's important to note that the token is sent as a header on every request. If you include the token in a page that is vulnerable to [BREACH](https://en.wikipedia.org/wiki/BREACH), an attacker may be able to extract the token. To mitigate this, ensure your pages are served over HTTPS, disable HTTP compression, and implement rate limiting for requests.
## Signatures
```go
func New(config ...Config) fiber.Handler
func TokenFromContext(c fiber.Ctx) string
func HandlerFromContext(c fiber.Ctx) *Handler
func (h *Handler) DeleteToken(c fiber.Ctx) error
```
## How to use Fiber's CSRF Middleware
## Examples
@ -118,8 +18,8 @@ Import the middleware package that is part of the Fiber web framework:
```go
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/csrf"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/csrf"
)
```
@ -131,12 +31,12 @@ app.Use(csrf.New())
// Or extend your config for customization
app.Use(csrf.New(csrf.Config{
KeyLookup: "header:X-Csrf-Token",
CookieName: "csrf_",
KeyLookup: "header:X-Csrf-Token",
CookieName: "csrf_",
CookieSameSite: "Lax",
Expiration: 1 * time.Hour,
KeyGenerator: utils.UUIDv4,
Extractor: func(c fiber.Ctx) (string, error) { ... },
Expiration: 1 * time.Hour,
KeyGenerator: utils.UUIDv4,
Extractor: func(c fiber.Ctx) (string, error) { ... },
}))
```
@ -148,35 +48,58 @@ Getting the CSRF token in a handler:
```go
func handler(c fiber.Ctx) error {
handler := csrf.HandlerFromContext(c)
token := csrf.TokenFromContext(c)
if handler == nil {
panic("csrf middleware handler not registered")
}
cfg := handler.Config
if cfg == nil {
panic("csrf middleware handler has no config")
}
handler := csrf.HandlerFromContext(c)
token := csrf.TokenFromContext(c)
if handler == nil {
panic("csrf middleware handler not registered")
}
cfg := handler.Config
if cfg == nil {
panic("csrf middleware handler has no config")
}
if !strings.Contains(cfg.KeyLookup, ":") {
panic("invalid KeyLookup format")
}
formKey := strings.Split(cfg.KeyLookup, ":")[1]
panic("invalid KeyLookup format")
}
formKey := strings.Split(cfg.KeyLookup, ":")[1]
tmpl := fmt.Sprintf(`<form action="/post" method="POST">
<input type="hidden" name="%s" value="%s">
<input type="text" name="message">
<input type="submit" value="Submit">
</form>`, formKey, token)
c.Set("Content-Type", "text/html")
return c.SendString(tmpl)
tmpl := fmt.Sprintf(`<form action="/post" method="POST">
<input type="hidden" name="%s" value="%s">
<input type="text" name="message">
<input type="submit" value="Submit">
</form>`, formKey, token)
c.Set("Content-Type", "text/html")
return c.SendString(tmpl)
}
```
## Recipes for Common Use Cases
There are two basic use cases for the CSRF middleware:
1. **Without Sessions**: This is the simplest way to use the middleware. It uses the Double Submit Cookie Pattern and does not require a user session.
- See GoFiber recipe [CSRF](https://github.com/gofiber/recipes/tree/master/csrf) for an example of using the CSRF middleware without a user session.
2. **With Sessions**: This is generally considered more secure. It uses the Synchronizer Token Pattern and requires a user session, and the use of pre-session, which prevents login CSRF attacks.
- See GoFiber recipe [CSRF with Session](https://github.com/gofiber/recipes/tree/master/csrf-with-session) for an example of using the CSRF middleware with a user session.
## Signatures
```go
func New(config ...Config) fiber.Handler
func TokenFromContext(c fiber.Ctx) string
func HandlerFromContext(c fiber.Ctx) *Handler
func (h *Handler) DeleteToken(c fiber.Ctx) error
```
## Config
| Property | Type | Description | Default |
|:------------------|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------|
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
| 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 csrf cookie. This cookie will store the csrf key. | "csrf_" |
| CookieDomain | `string` | Domain of the CSRF cookie. | "" |
@ -186,13 +109,14 @@ func handler(c fiber.Ctx) error {
| 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 |
| KeyGenerator | `func() string` | KeyGenerator creates a new CSRF token. | utils.UUID |
| 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 |
| 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. | "csrfToken" |
| KeyGenerator | `func() string` | KeyGenerator creates a new CSRF token. | utils.UUID |
| 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 |
| TrustedOrigins | `[]string` | TrustedOrigins is a list of trusted origins for unsafe requests. This supports subdomain matching, so you can use a value like "https://.example.com" to allow any subdomain of example.com to submit requests. | `[]` |
### Default Config
@ -246,8 +170,11 @@ The CSRF middleware utilizes a set of sentinel errors to handle various scenario
- `ErrTokenNotFound`: Indicates that the CSRF token was not found.
- `ErrTokenInvalid`: Indicates that the CSRF token is invalid.
- `ErrNoReferer`: Indicates that the referer was not supplied.
- `ErrBadReferer`: Indicates that the referer is invalid.
- `ErrRefererNotFound`: Indicates that the referer was not supplied.
- `ErrRefererInvalid`: Indicates that the referer is invalid.
- `ErrRefererNoMatch`: Indicates that the referer does not match host and is not a trusted origin.
- `ErrOriginInvalid`: Indicates that the origin is invalid.
- `ErrOriginNoMatch`: Indicates that the origin does not match host and is not a trusted origin.
If you use the default error handler, the client will receive a 403 Forbidden error without any additional information.
@ -285,3 +212,97 @@ app.Use(csrf.New(csrf.Config{
Storage: storage,
}))
```
# How It Works
## Token Generation
CSRF tokens are generated on 'safe' requests and when the existing token has expired or hasn't been set yet. If `SingleUseToken` is `true`, a new token is generated after each use. Retrieve the CSRF token using `csrf.TokenFromContext(c)`.
## Security Considerations
This middleware is designed to protect against CSRF attacks but does not protect against other attack vectors, such as XSS. It should be used in combination with other security measures.
:::danger
Never use 'safe' methods to mutate data, for example, never use a GET request to modify a resource. This middleware will not protect against CSRF attacks on 'safe' methods.
:::
### Token Validation Patterns
#### Double Submit Cookie Pattern (Default)
By default, the middleware generates and stores tokens using the `fiber.Storage` interface. These tokens are not linked to any particular user session, and they are validated using the Double Submit Cookie pattern. The token is stored in a cookie, and then sent as a header on requests. The middleware compares the cookie value with the header value to validate the token. This is a secure pattern that does not require a user session.
When the authorization status changes, the previously issued token MUST be deleted, and a new one generated. See [Token Lifecycle](#token-lifecycle) [Deleting Tokens](#deleting-tokens) for more information.
:::caution
When using this pattern, it's important to set the `CookieSameSite` option to `Lax` or `Strict` and ensure that the Extractor is not `FromCookie`, and KeyLookup is not `cookie:<name>`.
:::
:::note
When using this pattern, this middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for Storage saves data to memory. See [Custom Storage/Database](#custom-storagedatabase) for customizing the storage.
:::
#### Synchronizer Token Pattern (with Session)
When using this middleware with a user session, the middleware can be configured to store the token within the session. This method is recommended when using a user session, as it is generally more secure than the Double Submit Cookie Pattern.
When using this pattern it's important to regenerate the session when the authorization status changes, this will also delete the token. See: [Token Lifecycle](#token-lifecycle) for more information.
:::caution
Pre-sessions are required and will be created automatically if not present. Use a session value to indicate authentication instead of relying on the presence of a session.
:::
### Defense In Depth
When using this middleware, it's recommended to serve your pages over HTTPS, set the `CookieSecure` option to `true`, and set the `CookieSameSite` option to `Lax` or `Strict`. This ensures that the cookie is only sent over HTTPS and not on requests from external sites.
:::note
Cookie prefixes `__Host-` and `__Secure-` can be used to further secure the cookie. Note that these prefixes are not supported by all browsers and there are other limitations. See [MDN#Set-Cookie#cookie_prefixes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes) for more information.
To use these prefixes, set the `CookieName` option to `__Host-csrf_` or `__Secure-csrf_`.
:::
### Referer Checking
For HTTPS requests, this middleware performs strict referer checking. 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.
:::caution
When HTTPS requests are protected by CSRF, referer checking is always carried out.
The Referer header is automatically included in requests by all modern browsers, including those made using the JS Fetch API. However, if you're making use of this middleware with a custom client, it's important to ensure that the client sends a valid Referer header.
:::
### 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 extends the expiration by 1 hour. The token only expires if the user doesn't make a request for the duration of the expiration time.
#### Token Reuse
By default, tokens may be used multiple times. If you want to delete the token after it has been used, 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.
:::info
Using `SingleUseToken` comes with usability trade-offs and is not enabled by default. For example, it can interfere with the user experience if the user has multiple tabs open or uses the back button.
:::
#### Deleting Tokens
When the authorization status changes, the CSRF token MUST be deleted, and a new one generated. This can be done by calling `handler.DeleteToken(c)`.
```go
handler := csrf.HandlerFromContext(ctx)
if handler != nil {
if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil {
// handle error
}
}
```
:::tip
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 the session and the token stored therein.
:::
### BREACH
It's important to note that the token is sent as a header on every request. If you include the token in a page that is vulnerable to [BREACH](https://en.wikipedia.org/wiki/BREACH), an attacker may be able to extract the token. To mitigate this, ensure your pages are served over HTTPS, disable HTTP compression, and implement rate limiting for requests.

View File

@ -89,6 +89,19 @@ type Config struct {
// Default: "csrfToken"
SessionKey string
// TrustedOrigins is a list of trusted origins for unsafe requests.
// For requests that use the Origin header, the origin must match the
// Host header or one of the TrustedOrigins.
// For secure requests, that do not include the Origin header, the Referer
// header must match the Host header or one of the TrustedOrigins.
//
// This supports subdomain matching, so you can use a value like "https://.example.com"
// to allow any subdomain of example.com to submit requests.
//
//
// Optional. Default: []
TrustedOrigins []string
// KeyGenerator creates a new CSRF token
//
// Optional. Default: utils.UUID

View File

@ -4,20 +4,25 @@ import (
"errors"
"net/url"
"reflect"
"strings"
"time"
"github.com/gofiber/fiber/v3"
)
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{'+'}
ErrTokenNotFound = errors.New("csrf token not found")
ErrTokenInvalid = errors.New("csrf token invalid")
ErrRefererNotFound = errors.New("referer not supplied")
ErrRefererInvalid = errors.New("referer invalid")
ErrRefererNoMatch = errors.New("referer does not match host and is not a trusted origin")
ErrOriginInvalid = errors.New("origin invalid")
ErrOriginNoMatch = errors.New("origin does not match host and is not a trusted origin")
errOriginNotFound = errors.New("origin not supplied or is null") // internal error, will not be returned to the user
dummyValue = []byte{'+'}
)
// Handler handles
// Handler for CSRF middleware
type Handler struct {
config *Config
sessionManager *sessionManager
@ -82,13 +87,24 @@ func New(config ...Config) fiber.Handler {
default:
// Assume that anything not defined as 'safe' by RFC7231 needs protection
// Enforce an origin check for HTTPS connections.
if c.Scheme() == "https" {
if err := refererMatchesHost(c); err != nil {
return cfg.ErrorHandler(c, err)
// Enforce an origin check for unsafe requests.
err := originMatchesHost(c, cfg.TrustedOrigins)
// If there's no origin, enforce a referer check for HTTPS connections.
if errors.Is(err, errOriginNotFound) {
if c.Scheme() == "https" {
err = refererMatchesHost(c, cfg.TrustedOrigins)
} else {
// If it's not HTTPS, clear the error to allow the request to proceed.
err = nil
}
}
// If there's an error (either from origin check or referer check), handle it.
if err != nil {
return cfg.ErrorHandler(c, err)
}
// Extract token from client request i.e. header, query, param, form or cookie
extractedToken, err := cfg.Extractor(c)
if err != nil {
@ -243,23 +259,81 @@ func isFromCookie(extractor any) bool {
return reflect.ValueOf(extractor).Pointer() == reflect.ValueOf(FromCookie).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
// originMatchesHost checks that the origin header matches the host header
// returns an error if the origin header is not present or is invalid
// returns nil if the origin header is valid
func originMatchesHost(c fiber.Ctx, trustedOrigins []string) error {
origin := c.Get(fiber.HeaderOrigin)
if origin == "" || origin == "null" { // "null" is set by some browsers when the origin is a secure context https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin#description
return errOriginNotFound
}
refererURL, err := url.Parse(referer)
originURL, err := url.Parse(origin)
if err != nil {
return ErrBadReferer
return ErrOriginInvalid
}
if refererURL.Scheme+"://"+refererURL.Host != c.Scheme()+"://"+c.Host() {
return ErrBadReferer
if originURL.Host != c.Host() {
for _, trustedOrigin := range trustedOrigins {
if isTrustedSchemeAndDomain(trustedOrigin, origin) {
return nil
}
}
return ErrOriginNoMatch
}
return nil
}
// 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, trustedOrigins []string) error {
referer := c.Get(fiber.HeaderReferer)
if referer == "" {
return ErrRefererNotFound
}
refererURL, err := url.Parse(referer)
if err != nil {
return ErrRefererInvalid
}
if refererURL.Host != c.Host() {
for _, trustedOrigin := range trustedOrigins {
if isTrustedSchemeAndDomain(trustedOrigin, referer) {
return nil
}
}
return ErrRefererNoMatch
}
return nil
}
// isTrustedSchemeAndDomain checks if the trustedProtoDomain is the same as the protoDomain
// or if the protoDomain is a subdomain of the trustedProtoDomain where trustedProtoDomain
// is prefixed with "https://." or "http://."
func isTrustedSchemeAndDomain(trustedProtoDomain, protoDomain string) bool {
if trustedProtoDomain == protoDomain {
return true
}
// Use constant prefixes for better readability and avoid magic numbers.
const httpsPrefix = "https://."
const httpPrefix = "http://."
if strings.HasPrefix(trustedProtoDomain, httpsPrefix) {
trustedProtoDomain = trustedProtoDomain[len(httpsPrefix):]
protoDomain = strings.TrimPrefix(protoDomain, "https://")
return strings.HasSuffix(protoDomain, "."+trustedProtoDomain)
}
if strings.HasPrefix(trustedProtoDomain, httpPrefix) {
trustedProtoDomain = trustedProtoDomain[len(httpPrefix):]
protoDomain = strings.TrimPrefix(protoDomain, "http://")
return strings.HasSuffix(protoDomain, "."+trustedProtoDomain)
}
return false
}

View File

@ -40,7 +40,7 @@ func Test_CSRF(t *testing.T) {
h(ctx)
require.Equal(t, 403, ctx.Response.StatusCode())
// Empty/invalid CSRF token
// Invalid CSRF token
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
@ -598,6 +598,280 @@ func Test_CSRF_From_Custom(t *testing.T) {
require.Equal(t, 200, ctx.Response.StatusCode())
}
func Test_CSRF_Extractor_EmptyString(t *testing.T) {
t.Parallel()
app := fiber.New()
extractor := func(_ fiber.Ctx) (string, error) {
return "", nil
}
errorHandler := func(c fiber.Ctx, err error) error {
return c.Status(403).SendString(err.Error())
}
app.Use(New(Config{
Extractor: extractor,
ErrorHandler: errorHandler,
}))
app.Post("/", func(c fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
h := app.Handler()
ctx := &fasthttp.RequestCtx{}
// Generate CSRF token
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodGet)
h(ctx)
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
ctx.Request.Reset()
ctx.Response.Reset()
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)
require.Equal(t, 403, ctx.Response.StatusCode())
require.Equal(t, ErrTokenNotFound.Error(), string(ctx.Response.Body()))
}
func Test_CSRF_Origin(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, "http")
h(ctx)
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
// Test Correct Origin with port
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("example.com:8080")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("example.com:8080")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com:8080")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Correct Origin with wrong port
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("example.com")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com:3000")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 403, ctx.Response.StatusCode())
// Test Correct Origin with null
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("example.com")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "null")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Correct Origin with ReverseProxy
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("10.0.1.42.com:8080")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("10.0.1.42:8080")
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "http")
ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com")
ctx.Request.Header.Set(fiber.HeaderXForwardedFor, `192.0.2.43, "[2001:db8:cafe::17]"`)
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Correct Origin with ReverseProxy Missing X-Forwarded-* Headers
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("10.0.1.42:8080")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("10.0.1.42:8080")
ctx.Request.Header.Set(fiber.HeaderXUrlScheme, "http") // We need to set this header to make sure c.Protocol() returns http
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 403, ctx.Response.StatusCode())
// Test Correct Origin with path
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "http")
ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com/action/items?gogogo=true")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Wrong Origin
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "http")
ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://csrf.example.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 403, ctx.Response.StatusCode())
}
func Test_CSRF_TrustedOrigins(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
CookieSecure: true,
TrustedOrigins: []string{
"http://safe.example.com",
"https://safe.example.com",
"http://.domain-1.com",
"https://.domain-1.com",
},
}))
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 Trusted Origin
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("example.com")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://safe.example.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Trusted Origin Subdomain
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("domain-1.com")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("domain-1.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://safe.domain-1.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Trusted Origin Invalid
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("domain-1.com")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("domain-1.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://evildomain-1.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 403, ctx.Response.StatusCode())
// Test Trusted Referer
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
ctx.Request.URI().SetScheme("https")
ctx.Request.URI().SetHost("example.com")
ctx.Request.Header.SetProtocol("https")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set(fiber.HeaderReferer, "https://safe.example.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Trusted Referer Wildcard
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
ctx.Request.URI().SetScheme("https")
ctx.Request.URI().SetHost("domain-1.com")
ctx.Request.Header.SetProtocol("https")
ctx.Request.Header.SetHost("domain-1.com")
ctx.Request.Header.Set(fiber.HeaderReferer, "https://safe.domain-1.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 200, ctx.Response.StatusCode())
// Test Trusted Referer Invalid
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
ctx.Request.URI().SetScheme("https")
ctx.Request.URI().SetHost("api.domain-1.com")
ctx.Request.Header.SetProtocol("https")
ctx.Request.Header.SetHost("api.domain-1.com")
ctx.Request.Header.Set(fiber.HeaderReferer, "https://evildomain-1.com")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 403, ctx.Response.StatusCode())
}
func Test_CSRF_Referer(t *testing.T) {
t.Parallel()
app := fiber.New()
@ -620,6 +894,7 @@ func Test_CSRF_Referer(t *testing.T) {
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
ctx.Request.URI().SetScheme("https")
ctx.Request.URI().SetHost("example.com:8443")
ctx.Request.Header.SetProtocol("https")
@ -634,6 +909,7 @@ func Test_CSRF_Referer(t *testing.T) {
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
ctx.Request.URI().SetScheme("https")
ctx.Request.URI().SetHost("10.0.1.42.com:8443")
ctx.Request.Header.SetProtocol("https")
@ -651,6 +927,7 @@ func Test_CSRF_Referer(t *testing.T) {
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
ctx.Request.URI().SetScheme("https")
ctx.Request.URI().SetHost("10.0.1.42:8443")
ctx.Request.Header.SetProtocol("https")
@ -867,7 +1144,7 @@ func Test_CSRF_ErrorHandler_MissingReferer(t *testing.T) {
app := fiber.New()
errHandler := func(ctx fiber.Ctx, err error) error {
require.Equal(t, ErrNoReferer, err)
require.Equal(t, ErrRefererNotFound, err)
return ctx.Status(419).Send([]byte("empty CSRF token"))
}
@ -1040,3 +1317,116 @@ func Benchmark_Middleware_CSRF_GenerateToken(b *testing.B) {
require.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())
}
func Test_CSRF_InvalidURLHeaders(t *testing.T) {
t.Parallel()
app := fiber.New()
errHandler := func(ctx fiber.Ctx, err error) error {
return ctx.Status(419).Send([]byte(err.Error()))
}
app.Use(New(Config{ErrorHandler: errHandler}))
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)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "http")
h(ctx)
token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))
token = strings.Split(strings.Split(token, ";")[0], "=")[1]
// invalid Origin
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.URI().SetScheme("http")
ctx.Request.URI().SetHost("example.com")
ctx.Request.Header.SetProtocol("http")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set(fiber.HeaderOrigin, "http://[::1]:%38%30/Invalid Origin")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 419, ctx.Response.StatusCode())
require.Equal(t, ErrOriginInvalid.Error(), string(ctx.Response.Body()))
// invalid Referer
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.Header.SetMethod(fiber.MethodPost)
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
ctx.Request.URI().SetScheme("https")
ctx.Request.URI().SetHost("example.com")
ctx.Request.Header.SetProtocol("https")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set(fiber.HeaderReferer, "http://[::1]:%38%30/Invalid Referer")
ctx.Request.Header.Set(HeaderName, token)
ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)
h(ctx)
require.Equal(t, 419, ctx.Response.StatusCode())
require.Equal(t, ErrRefererInvalid.Error(), string(ctx.Response.Body()))
}
func Test_CSRF_TokenFromContext(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New())
app.Get("/", func(c fiber.Ctx) error {
token := TokenFromContext(c)
require.NotEmpty(t, token)
return c.SendStatus(fiber.StatusOK)
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
}
func Test_CSRF_FromContextMethods(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New())
app.Get("/", func(c fiber.Ctx) error {
token := TokenFromContext(c)
require.NotEmpty(t, token)
handler := HandlerFromContext(c)
require.NotNil(t, handler)
return c.SendStatus(fiber.StatusOK)
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
}
func Test_CSRF_FromContextMethods_Invalid(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
token := TokenFromContext(c)
require.Empty(t, token)
handler := HandlerFromContext(c)
require.Nil(t, handler)
return c.SendStatus(fiber.StatusOK)
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
}