mirror of https://github.com/gofiber/fiber.git
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
parent
7bc43dcabf
commit
fcb853788b
|
@ -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.
|
As a [Defense In Depth](#defense-in-depth) measure, this middleware performs [Referer Checking](#referer-checking) for HTTPS requests.
|
||||||
|
|
||||||
## Token Generation
|
## How to use Fiber's CSRF Middleware
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -172,6 +72,29 @@ func handler(c fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
## Config
|
||||||
|
|
||||||
| Property | Type | Description | Default |
|
| Property | Type | Description | Default |
|
||||||
|
@ -186,13 +109,14 @@ func handler(c fiber.Ctx) error {
|
||||||
| CookieSameSite | `string` | Value of SameSite cookie. | "Lax" |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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` |
|
| 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` |
|
| 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" |
|
| 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 |
|
| 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. | `[]` |
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Default Config
|
### 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.
|
- `ErrTokenNotFound`: Indicates that the CSRF token was not found.
|
||||||
- `ErrTokenInvalid`: Indicates that the CSRF token is invalid.
|
- `ErrTokenInvalid`: Indicates that the CSRF token is invalid.
|
||||||
- `ErrNoReferer`: Indicates that the referer was not supplied.
|
- `ErrRefererNotFound`: Indicates that the referer was not supplied.
|
||||||
- `ErrBadReferer`: Indicates that the referer is invalid.
|
- `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.
|
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,
|
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.
|
||||||
|
|
|
@ -89,6 +89,19 @@ type Config struct {
|
||||||
// Default: "csrfToken"
|
// Default: "csrfToken"
|
||||||
SessionKey string
|
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
|
// KeyGenerator creates a new CSRF token
|
||||||
//
|
//
|
||||||
// Optional. Default: utils.UUID
|
// Optional. Default: utils.UUID
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
@ -12,12 +13,16 @@ import (
|
||||||
var (
|
var (
|
||||||
ErrTokenNotFound = errors.New("csrf token not found")
|
ErrTokenNotFound = errors.New("csrf token not found")
|
||||||
ErrTokenInvalid = errors.New("csrf token invalid")
|
ErrTokenInvalid = errors.New("csrf token invalid")
|
||||||
ErrNoReferer = errors.New("referer not supplied")
|
ErrRefererNotFound = errors.New("referer not supplied")
|
||||||
ErrBadReferer = errors.New("referer invalid")
|
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{'+'}
|
dummyValue = []byte{'+'}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler handles
|
// Handler for CSRF middleware
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
config *Config
|
config *Config
|
||||||
sessionManager *sessionManager
|
sessionManager *sessionManager
|
||||||
|
@ -82,13 +87,24 @@ func New(config ...Config) fiber.Handler {
|
||||||
default:
|
default:
|
||||||
// Assume that anything not defined as 'safe' by RFC7231 needs protection
|
// Assume that anything not defined as 'safe' by RFC7231 needs protection
|
||||||
|
|
||||||
// Enforce an origin check for HTTPS connections.
|
// 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" {
|
if c.Scheme() == "https" {
|
||||||
if err := refererMatchesHost(c); err != nil {
|
err = refererMatchesHost(c, cfg.TrustedOrigins)
|
||||||
return cfg.ErrorHandler(c, err)
|
} 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
|
// Extract token from client request i.e. header, query, param, form or cookie
|
||||||
extractedToken, err := cfg.Extractor(c)
|
extractedToken, err := cfg.Extractor(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -243,23 +259,81 @@ func isFromCookie(extractor any) bool {
|
||||||
return reflect.ValueOf(extractor).Pointer() == reflect.ValueOf(FromCookie).Pointer()
|
return reflect.ValueOf(extractor).Pointer() == reflect.ValueOf(FromCookie).Pointer()
|
||||||
}
|
}
|
||||||
|
|
||||||
// refererMatchesHost checks that the referer header matches the host header
|
// originMatchesHost checks that the origin header matches the host header
|
||||||
// returns an error if the referer header is not present or is invalid
|
// returns an error if the origin header is not present or is invalid
|
||||||
// returns nil if the referer header is valid
|
// returns nil if the origin header is valid
|
||||||
func refererMatchesHost(c fiber.Ctx) error {
|
func originMatchesHost(c fiber.Ctx, trustedOrigins []string) error {
|
||||||
referer := c.Get(fiber.HeaderReferer)
|
origin := c.Get(fiber.HeaderOrigin)
|
||||||
if referer == "" {
|
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 ErrNoReferer
|
return errOriginNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
refererURL, err := url.Parse(referer)
|
originURL, err := url.Parse(origin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrBadReferer
|
return ErrOriginInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if refererURL.Scheme+"://"+refererURL.Host != c.Scheme()+"://"+c.Host() {
|
if originURL.Host != c.Host() {
|
||||||
return ErrBadReferer
|
for _, trustedOrigin := range trustedOrigins {
|
||||||
|
if isTrustedSchemeAndDomain(trustedOrigin, origin) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ErrOriginNoMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func Test_CSRF(t *testing.T) {
|
||||||
h(ctx)
|
h(ctx)
|
||||||
require.Equal(t, 403, ctx.Response.StatusCode())
|
require.Equal(t, 403, ctx.Response.StatusCode())
|
||||||
|
|
||||||
// Empty/invalid CSRF token
|
// Invalid CSRF token
|
||||||
ctx.Request.Reset()
|
ctx.Request.Reset()
|
||||||
ctx.Response.Reset()
|
ctx.Response.Reset()
|
||||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
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())
|
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) {
|
func Test_CSRF_Referer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
@ -620,6 +894,7 @@ func Test_CSRF_Referer(t *testing.T) {
|
||||||
ctx.Request.Reset()
|
ctx.Request.Reset()
|
||||||
ctx.Response.Reset()
|
ctx.Response.Reset()
|
||||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||||
|
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||||
ctx.Request.URI().SetScheme("https")
|
ctx.Request.URI().SetScheme("https")
|
||||||
ctx.Request.URI().SetHost("example.com:8443")
|
ctx.Request.URI().SetHost("example.com:8443")
|
||||||
ctx.Request.Header.SetProtocol("https")
|
ctx.Request.Header.SetProtocol("https")
|
||||||
|
@ -634,6 +909,7 @@ func Test_CSRF_Referer(t *testing.T) {
|
||||||
ctx.Request.Reset()
|
ctx.Request.Reset()
|
||||||
ctx.Response.Reset()
|
ctx.Response.Reset()
|
||||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||||
|
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||||
ctx.Request.URI().SetScheme("https")
|
ctx.Request.URI().SetScheme("https")
|
||||||
ctx.Request.URI().SetHost("10.0.1.42.com:8443")
|
ctx.Request.URI().SetHost("10.0.1.42.com:8443")
|
||||||
ctx.Request.Header.SetProtocol("https")
|
ctx.Request.Header.SetProtocol("https")
|
||||||
|
@ -651,6 +927,7 @@ func Test_CSRF_Referer(t *testing.T) {
|
||||||
ctx.Request.Reset()
|
ctx.Request.Reset()
|
||||||
ctx.Response.Reset()
|
ctx.Response.Reset()
|
||||||
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
ctx.Request.Header.SetMethod(fiber.MethodPost)
|
||||||
|
ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https")
|
||||||
ctx.Request.URI().SetScheme("https")
|
ctx.Request.URI().SetScheme("https")
|
||||||
ctx.Request.URI().SetHost("10.0.1.42:8443")
|
ctx.Request.URI().SetHost("10.0.1.42:8443")
|
||||||
ctx.Request.Header.SetProtocol("https")
|
ctx.Request.Header.SetProtocol("https")
|
||||||
|
@ -867,7 +1144,7 @@ func Test_CSRF_ErrorHandler_MissingReferer(t *testing.T) {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
|
||||||
errHandler := func(ctx fiber.Ctx, err error) error {
|
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"))
|
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())
|
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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue