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.
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue