1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-05-11 18:26:34 +00:00
RW 60967f5a95
Main v2 merge ()
* Update pull_request_template.md

* Update v3-changes.md

* Update CONTRIBUTING.md ()

Grammar correction.

* chore(encryptcookie)!: update default config ()

* chore(encryptcookie)!: update default config

docs(encryptcookie): enhance documentation and examples

BREAKING CHANGE: removed the hardcoded "csrf_" from the Except.

* docs(encryptcookie): reads or modifies cookies

* chore(encryptcookie): csrf config example

* docs(encryptcookie): md table spacing

* build(deps): bump actions/setup-go from 4 to 5 ()

Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* 🩹 middleware/logger/: log client IP address by default ()

* middleware/logger: Log client IP address by default.

* Update doc.

* fix: don't constrain middlewares' context-keys to strings 🐛 ()

* Revert "Revert "🐛 requestid.Config.ContextKey is interface{} ()" ()"

This reverts commit 28be17f929cfa7d3c27dd292fc3956f2f9882e22.

* fix: request ContextKey default value condition

Should check for `nil` since it is `any`.

* fix: don't constrain middlewares' context-keys to strings

`context` recommends using "unexported type" as context keys to avoid
collisions https://pkg.go.dev/github.com/gofiber/fiber/v2#Ctx.Locals.

The official go blog also recommends this https://go.dev/blog/context.

`fiber.Ctx.Locals(key any, value any)` correctly allows consumers to
use unexported types or e.g. strings.

But some fiber middlewares constrain their context-keys to `string` in
their "default config structs", making it impossible to use unexported
types.

This PR removes the `string` _constraint_ from all middlewares, allowing
to now use unexported types as per the official guidelines. However
the default value is still a string, so it's not a breaking change, and
anyone still using strings as context keys is not affected.

* 📚 Update app.md for indentation ()

Update app.md for indentation

* build(deps): bump github.com/google/uuid from 1.4.0 to 1.5.0 ()

Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump github/codeql-action from 2 to 3 ()

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Changing default log output ()

changing default log output

Closes 

* Update hooks.md

fix wrong hooks signature

* 🩹 Fix: CORS middleware should use the defined AllowedOriginsFunc config when AllowedOrigins is empty ()

* 🐛 [Bug]: Adaptator + otelfiber issue  ()

* 🩹🚨 - fix for redirect with query params ()

* redirect with query params did not work, fix it and add test for it

* redirect middleware - fix test typo

* ♻️ logger/middleware colorize logger error message  ()

*  feat: add liveness and readiness checks ()

*  feat: add liveness and readiness checkers

* 📝 docs: add docs for liveness and readiness

*  feat: add options method for probe checkers

*  tests: add tests for liveness and readiness

* ♻️ refactor: change default endpoint values

* ♻️ refactor: change default value for liveness endpoint

* 📝 docs: add return status for liveness and readiness probes

* ♻️ refactor: change probechecker to middleware

* 📝 docs: move docs to middleware session

* ♻️ refactor: apply gofumpt formatting

* ♻️ refactor: remove unused parameter

* split config and apply a review

* apply reviews and add testcases

* add benchmark

* cleanup

* rename middleware

* fix linter

* Update docs and config values

* Revert change to IsReady

* Updates based on code review

* Update docs to match other middlewares

---------

Co-authored-by: Muhammed Efe Cetin <efectn@protonmail.com>
Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
Co-authored-by: Juan Calderon-Perez <jgcalderonperez@protonmail.com>

* prepare release v2.52.0
- add more Parser tests

* fix healthcheck.md

* configure workflows for V2 branch

* configure workflows for V2 branch

* Fix default value to false in docs of QueryBool ()

fix default value to false in docs of QueryBool

* update queryParser config

* Update ctx.md

* Update routing.md

* 📚 Doc: Fix code snippet indentation in /docs/api/middleware/keyauth.md

Removes an an extra level of indentation in line 51 of
`keyauth.md` [here](https://github.com/gofiber/fiber/blob/v2/docs/api/middleware/keyauth.md?plain=1#L51)

* fix: healthcheck middleware not working with route group ()

* fix: healthcheck middleware not working with route group

* perf: change verification method to improve perf

* Update healthcheck_test.go

* test: add not matching route test for strict routing

* add more test cases

* correct tests

* correct test helpers

* correct tests

* correct tests

---------

Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
Co-authored-by: René Werner <rene@gofiber.io>

* Merge pull request from GHSA-fmg4-x8pw-hjhg

* Enforce Wildcard Origins with AllowCredentials check

* Expand unit-tests, fix issues with subdomains logic, update docs

* Update cors.md

* Added test using localhost, ipv4, and ipv6 address

* improve documentation markdown

---------

Co-authored-by: René Werner <rene@gofiber.io>

* Update app.go

prepare release v2.52.1

* fix cors domain normalize

* fix sync-docs workflow

* fix sync-docs workflow

* fix(middleware/cors): Validation of multiple Origins ()

* fix: allow origins check

Refactor CORS origin validation and normalization to trim leading or trailing whitespace in the cfg.AllowOrigins string [list]. URLs with whitespace inside the URL are invalid, so the normalizeOrigin will return false because url.Parse will fail, and the middleware will panic.

fixes 

* test: AllowOrigins with whitespace

* test(middleware/cors): add benchmarks

* chore: fix linter errors

* test(middleware/cors): use h() instead of app.Test()

* test(middleware/cors): add miltiple origins in Test_CORS_AllowOriginScheme

* chore: refactor validate and normalize

* test(cors/middleware): add more benchmarks

* prepare release v2.52.2

* refactor(docs): deactivate docs sync for v2

* refactor(docs): deactivate docs sync for v2

* fix(middleware/cors): Handling and wildcard subdomain matching ()

* fix: allow origins check

Refactor CORS origin validation and normalization to trim leading or trailing whitespace in the cfg.AllowOrigins string [list]. URLs with whitespace inside the URL are invalid, so the normalizeOrigin will return false because url.Parse will fail, and the middleware will panic.

fixes 

* test: AllowOrigins with whitespace

* test(middleware/cors): add benchmarks

* chore: fix linter errors

* test(middleware/cors): use h() instead of app.Test()

* test(middleware/cors): add miltiple origins in Test_CORS_AllowOriginScheme

* chore: refactor validate and normalize

* test(cors/middleware): add more benchmarks

* fix(middleware/cors): handling and wildcard subdomain matching

docs(middleware/cors): add How it works and Security Considerations

* chore: grammar

* Apply suggestions from code review

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

* chore: fix misspelling

* test(middleware/cors): combine Invalid_Origins tests

* refactor(middleware/cors): headers handling

* docs(middleware/cors): Update AllowOrigins description

* chore: merge

* perf(middleware/cors): optimize handler

* perf(middleware/cors): optimize handler

* chore(middleware/cors): ipdate origin handling logic

* chore(middleware/cors): fix header capitalization

* docs(middleware/cors): improve sercuity notes

* docs(middleware/cors): Improve security notes

* docs(middleware/cors): improve CORS overview

* docs(middleware/cors): fix ordering of how it works

* docs(middleware/cors): add additional info to How to works

* docs(middleware/cors): rm space

* docs(middleware/cors): add validation for AllowOrigins origins to overview

* docs(middleware/cors): update ExposeHeaders and MaxAge descriptions

* docs(middleware/cors): Add dynamic origin validation example

* docs(middleware/cors): Improve security notes and fix header capitalization

* docs(middleware/cors): configuration examples

* docs(middleware/cors): `"*"`

---------

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

* fix(middleware/cors): Categorize requests correctly ()

* fix(middleware/cors): categorise requests correctly

* test(middleware/cors): improve test coverage for request types

* test(middleware/cors): Add subdomain matching tests

* test(middleware/cors): parallel tests for CORS headers based on request type

* test(middleware/cors): Add benchmark for CORS subdomain matching

* test(middleware/cors): cover additiona test cases

* refactor(middleware/cors): origin validation and normalization

* test(middleware/csrf): Fix Benchmark Tests ()

* test(middleware/csrf): fix Benchmark_Middleware_CSRF_*

* fix(middleware/csrf): update refererMatchesHost()

* Prepare release v2.52.3

* fix(middleware/cors): CORS handling ()

* fix(middleware/cors): CORS handling

* fix(middleware/cors): Vary header handling

* test(middleware/cors): Ensure Vary Headers checked

* fix(middleware/cors): Vary header handling non-cors OPTIONS requests ()

* fix(middleware/cors): Vary header handling non-cors OPTIONS requests

* chore(middleware/cors): Add Vary header for non-CORS OPTIONS requests comment

* prepare release v2.52.4

* merge v2 in main(v3)

* merge v2 in main(v3)

* merge v2 in main(v3)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tokelo-12 <113810058+tokelo-12@users.noreply.github.com>
Co-authored-by: Jason McNeil <sixcolors@mac.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: iRedMail <2048991+iredmail@users.noreply.github.com>
Co-authored-by: Benjamin Grosse <ste3ls@gmail.com>
Co-authored-by: Mehmet Firat KOMURCU <mehmetfiratkomurcu@hotmail.com>
Co-authored-by: Bruno <bdm2943@icloud.com>
Co-authored-by: Muhammad Kholid B <muhammadkholidb@gmail.com>
Co-authored-by: gilwo <gilwo@users.noreply.github.com>
Co-authored-by: Lucas Lemos <lucashenriqueblemos@gmail.com>
Co-authored-by: Muhammed Efe Cetin <efectn@protonmail.com>
Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
Co-authored-by: Juan Calderon-Perez <jgcalderonperez@protonmail.com>
Co-authored-by: Jongmin Kim <kjongmin26@gmail.com>
Co-authored-by: Giovanni Rivera <rivera.giovanni271@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-03-28 10:39:12 +01:00

350 lines
10 KiB
Go

package csrf
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")
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 for CSRF middleware
type Handler struct {
config Config
sessionManager *sessionManager
storageManager *storageManager
}
// The contextKey type is unexported to prevent collisions with context keys defined in
// other packages.
type contextKey int
// The keys for the values in context
const (
tokenKey contextKey = iota
handlerKey
)
// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
// Set default config
cfg := configDefault(config...)
// Create manager to simplify storage operations ( see *_manager.go )
var sessionManager *sessionManager
var storageManager *storageManager
if cfg.Session != nil {
// Register the Token struct in the session store
cfg.Session.RegisterType(Token{})
sessionManager = newSessionManager(cfg.Session, cfg.SessionKey)
} else {
storageManager = newStorageManager(cfg.Storage)
}
// Pre-parse trusted origins
trustedOrigins := []string{}
trustedSubOrigins := []subdomain{}
for _, origin := range cfg.TrustedOrigins {
if i := strings.Index(origin, "://*."); i != -1 {
trimmedOrigin := strings.TrimSpace(origin[:i+3] + origin[i+4:])
isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin)
if !isValid {
panic("[CSRF] Invalid origin format in configuration:" + origin)
}
sd := subdomain{prefix: normalizedOrigin[:i+3], suffix: normalizedOrigin[i+3:]}
trustedSubOrigins = append(trustedSubOrigins, sd)
} else {
trimmedOrigin := strings.TrimSpace(origin)
isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin)
if !isValid {
panic("[CSRF] Invalid origin format in configuration:" + origin)
}
trustedOrigins = append(trustedOrigins, normalizedOrigin)
}
}
// Create the handler outside of the returned function
handler := &Handler{
config: cfg,
sessionManager: sessionManager,
storageManager: storageManager,
}
// Return new handler
return func(c fiber.Ctx) error {
// Don't execute middleware if Next returns true
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}
// Store the CSRF handler in the context
c.Locals(handlerKey, handler)
var token string
// Action depends on the HTTP method
switch c.Method() {
case fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace:
cookieToken := c.Cookies(cfg.CookieName)
if cookieToken != "" {
raw := getRawFromStorage(c, cookieToken, cfg, sessionManager, storageManager)
if raw != nil {
token = cookieToken // Token is valid, safe to set it
}
}
default:
// Assume that anything not defined as 'safe' by RFC7231 needs protection
// Enforce an origin check for unsafe requests.
err := originMatchesHost(c, trustedOrigins, trustedSubOrigins)
// If there's no origin, enforce a referer check for HTTPS connections.
if errors.Is(err, errOriginNotFound) {
if c.Scheme() == "https" {
err = refererMatchesHost(c, trustedOrigins, trustedSubOrigins)
} 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 {
return cfg.ErrorHandler(c, err)
}
if extractedToken == "" {
return cfg.ErrorHandler(c, ErrTokenNotFound)
}
// If not using FromCookie extractor, check that the token matches the cookie
// This is to prevent CSRF attacks by using a Double Submit Cookie method
// Useful when we do not have access to the users Session
if !isFromCookie(cfg.Extractor) && !compareStrings(extractedToken, c.Cookies(cfg.CookieName)) {
return cfg.ErrorHandler(c, ErrTokenInvalid)
}
raw := getRawFromStorage(c, extractedToken, cfg, sessionManager, storageManager)
if raw == nil {
// If token is not in storage, expire the cookie
expireCSRFCookie(c, cfg)
// and return an error
return cfg.ErrorHandler(c, ErrTokenNotFound)
}
if cfg.SingleUseToken {
// If token is single use, delete it from storage
deleteTokenFromStorage(c, extractedToken, cfg, sessionManager, storageManager)
} else {
token = extractedToken // Token is valid, safe to set it
}
}
// Generate CSRF token if not exist
if token == "" {
// And generate a new token
token = cfg.KeyGenerator()
}
// Create or extend the token in the storage
createOrExtendTokenInStorage(c, token, cfg, sessionManager, storageManager)
// Update the CSRF cookie
updateCSRFCookie(c, cfg, token)
// Tell the browser that a new header value is generated
c.Vary(fiber.HeaderCookie)
// Store the token in the context
c.Locals(tokenKey, token)
// Continue stack
return c.Next()
}
}
// TokenFromContext returns the token found in the context
// returns an empty string if the token does not exist
func TokenFromContext(c fiber.Ctx) string {
token, ok := c.Locals(tokenKey).(string)
if !ok {
return ""
}
return token
}
// HandlerFromContext returns the Handler found in the context
// returns nil if the handler does not exist
func HandlerFromContext(c fiber.Ctx) *Handler {
handler, ok := c.Locals(handlerKey).(*Handler)
if !ok {
return nil
}
return handler
}
// getRawFromStorage returns the raw value from the storage for the given token
// returns nil if the token does not exist, is expired or is invalid
func getRawFromStorage(c fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) []byte {
if cfg.Session != nil {
return sessionManager.getRaw(c, token, dummyValue)
}
return storageManager.getRaw(token)
}
// createOrExtendTokenInStorage creates or extends the token in the storage
func createOrExtendTokenInStorage(c fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) {
if cfg.Session != nil {
sessionManager.setRaw(c, token, dummyValue, cfg.Expiration)
} else {
storageManager.setRaw(token, dummyValue, cfg.Expiration)
}
}
func deleteTokenFromStorage(c fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) {
if cfg.Session != nil {
sessionManager.delRaw(c)
} else {
storageManager.delRaw(token)
}
}
// Update CSRF cookie
// if expireCookie is true, the cookie will expire immediately
func updateCSRFCookie(c fiber.Ctx, cfg Config, token string) {
setCSRFCookie(c, cfg, token, cfg.Expiration)
}
func expireCSRFCookie(c fiber.Ctx, cfg Config) {
setCSRFCookie(c, cfg, "", -time.Hour)
}
func setCSRFCookie(c fiber.Ctx, cfg Config, token string, expiry time.Duration) {
cookie := &fiber.Cookie{
Name: cfg.CookieName,
Value: token,
Domain: cfg.CookieDomain,
Path: cfg.CookiePath,
Secure: cfg.CookieSecure,
HTTPOnly: cfg.CookieHTTPOnly,
SameSite: cfg.CookieSameSite,
SessionOnly: cfg.CookieSessionOnly,
Expires: time.Now().Add(expiry),
}
// Set the CSRF cookie to the response
c.Cookie(cookie)
}
// DeleteToken removes the token found in the context from the storage
// and expires the CSRF cookie
func (handler *Handler) DeleteToken(c fiber.Ctx) error {
// Extract token from the client request cookie
cookieToken := c.Cookies(handler.config.CookieName)
if cookieToken == "" {
return handler.config.ErrorHandler(c, ErrTokenNotFound)
}
// Remove the token from storage
deleteTokenFromStorage(c, cookieToken, handler.config, handler.sessionManager, handler.storageManager)
// Expire the cookie
expireCSRFCookie(c, handler.config)
return nil
}
// isFromCookie checks if the extractor is set to ExtractFromCookie
func isFromCookie(extractor any) bool {
return reflect.ValueOf(extractor).Pointer() == reflect.ValueOf(FromCookie).Pointer()
}
// 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, trustedSubOrigins []subdomain) error {
origin := strings.ToLower(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
}
originURL, err := url.Parse(origin)
if err != nil {
return ErrOriginInvalid
}
if originURL.Scheme == c.Scheme() && originURL.Host == c.Host() {
return nil
}
for _, trustedOrigin := range trustedOrigins {
if origin == trustedOrigin {
return nil
}
}
for _, trustedSubOrigin := range trustedSubOrigins {
if trustedSubOrigin.match(origin) {
return nil
}
}
return ErrOriginNoMatch
}
// 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, trustedSubOrigins []subdomain) error {
referer := strings.ToLower(c.Get(fiber.HeaderReferer))
if referer == "" {
return ErrRefererNotFound
}
refererURL, err := url.Parse(referer)
if err != nil {
return ErrRefererInvalid
}
if refererURL.Scheme == c.Scheme() && refererURL.Host == c.Host() {
return nil
}
referer = refererURL.String()
for _, trustedOrigin := range trustedOrigins {
if referer == trustedOrigin {
return nil
}
}
for _, trustedSubOrigin := range trustedSubOrigins {
if trustedSubOrigin.match(referer) {
return nil
}
}
return ErrRefererNoMatch
}