Golang CSRF Defense in Practice
Tags: beginners, go, webdev, opensource
Published: 2022-12-04 08:17:14

## Hertz

[Hertz](https://github.com/cloudwego/hertz) is an ultra-large-scale enterprise-level microservice HTTP framework, featuring high ease of use, easy expansion, and low latency etc.

Hertz uses the self-developed high-performance network library Netpoll by default. In some special scenarios, Hertz has certain advantages in QPS and latency compared to go net.

In internal practice, some typical services, such as services with a high proportion of frameworks, gateways and other services, after migrating Hertz, compared to the Gin framework, the resource usage is significantly reduced, **CPU** **usage is reduced by 30%-60% with the size of the traffic**.

> For more details, see [cloudwego/hertz](https://github.com/cloudwego/hertz).

## CSRF

**Cross-site request forgery** (English: Cross-site request forgery), also known as **one-click attack** or **session riding**, usually abbreviated as **CSRF** or **XSRF**, is an attack method that coerces users to perform unintended operations on the currently logged-in web application. Compared with cross-site scripting (XSS), **XSS** utilizes the user's trust in the specified website, and CSRF utilizes the website's trust in the user's web browser.

## Hertz CSRF in action

Using a reverse proxy in Hertz requires pulling the [CSRF](https://github.com/hertz-contrib/csrf) extension provided by the community.

```Shell
$ go get github.com/hertz-contrib/csrf
```

### Basic use

```go
package main

import (
	"context"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/hertz-contrib/csrf"
	"github.com/hertz-contrib/sessions"
	"github.com/hertz-contrib/sessions/cookie"
)

func main() {
	h := server.Default()

	store := cookie.NewStore([]byte("secret"))
	h.Use(sessions.New("session", store))
	h.Use(csrf.New())

	h.GET("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, csrf.GetToken(ctx))
	})

	h.POST("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, "CSRF token is valid")
	})

	h.Spin()
}
```

First, we call sessions to expand and customize a session for testing, because subsequent tokens are generated through sessions. Then use the CSRF middleware directly.

We register two routes for testing, first use the GET method to call the `GetToken()` function to obtain the token generated by the CSRF middleware. Since we did not customize the `KeyLookup` option, the default value is `header:X-CSRF-TOKEN`, we put the obtained token into the header whose Key is `X-CSRF-TOKEN`, if If the token is invalid or the Key value is set incorrectly, calling `ErrorFunc` will return an error.

**Test**

```shell
$ curl 127.0.0.1:8888/protected

UMhM-eqB9CYjeuZO5o-9wJsQhb8KLQUpcRlYQnYagT4=
```

```shell
$ curl -X POST 127.0.0.1:8888/protected -H "X-CSRF-TOKEN=UMhM-eqB9CYjeuZO5o-9wJsQhb8KLQUpcRlYQnYagT4="

CSRF token is valid
```

### Custom configuration

| Configuration Item | Default                                                      | Description                                                  |
| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Secret             | "csrfSecret"                                                 | Used to generate tokens (required configuration)             |
| IgnoreMethods      | "GET", "HEAD", "OPTIONS", "TRACE"                            | Ignored methods will be treated as not requiring CSRF protection |
| Next               | nil                                                          | Next defines a function that, when true, skips the CSRF middleware. |
| KeyLookup          | header: X-CSRF-TOKEN                                         | KeyLookup is a string of the form "<source>:<key>" used to create an Extractor that extracts the token from the request. |
| ErrorFunc          | `func(ctx context.Context, c *app.RequestContext) { panic(c.Errors.Last()) }` | ErrorFunc is executed when app.HandlerFunc returns an error  |
| Extractor          | Created based on KeyLookup                                   | Extractor returns csrf token. If set, it will be used instead of KeyLookup based Extractor. |

#### WithSecret

```go
package main

import (
	"context"
	
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/hertz-contrib/csrf"
	"github.com/hertz-contrib/sessions"
	"github.com/hertz-contrib/sessions/cookie"
)

func main() {
	h := server.Default()
  
	store := cookie.NewStore([]byte("store"))
	h.Use(sessions.New("csrf-session", store))
	h.Use(csrf.New(csrf.WithSecret("your_secret")))
  
	h.GET("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, csrf.GetToken(ctx))
	})
  
	h.POST("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, "CSRF token is valid")
	})
	h.Spin()
}
```

`WithSecret` is used to help users set a custom secret for issuing tokens. The security of token generation can be improved by customizing the secret.

#### WithIgnoredMethods

```go
package main

import (
	"context"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/hertz-contrib/csrf"
	"github.com/hertz-contrib/sessions"
	"github.com/hertz-contrib/sessions/cookie"
)

func main() {
	h := server.Default()

	store := cookie.NewStore([]byte("secret"))
	h.Use(sessions.New("session", store))
	h.Use(csrf.New(csrf.WithIgnoredMethods([]string{"GET", "HEAD", "TRACE"})))

	h.GET("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, csrf.GetToken(ctx))
	})

	h.OPTIONS("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, "success")
	})
	h.Spin()
}
```

In RFC7231, GET, HEAD, OPTIONS, and TRACE methods are recognized as safe methods, so CSRF middleware is not used in these four methods by default. If you have other requirements during use, you can configure the ignored method. In the above code, the ignore of the OPTIONS method is canceled, so direct access to this interface through the OPTIONS method is not allowed.

#### WithErrorFunc

```go
package main

import (
	"context"
	"fmt"
	"net/http"
	
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/hertz-contrib/csrf"
	"github.com/hertz-contrib/sessions"
	"github.com/hertz-contrib/sessions/cookie"

)

func myErrFunc(c context.Context, ctx *app.RequestContext) {
	if ctx.Errors.Last() == nil {
		fmt.Errorf("myErrFunc called when no error occurs")
	}
	ctx.AbortWithMsg(ctx.Errors.Last().Error(), http.StatusBadRequest)
}

func main() {
	h := server.Default()

	store := cookie.NewStore([]byte("store"))
	h.Use(sessions.New("csrf-session", store))
	h.Use(csrf.New(csrf.WithErrorFunc(myErrFunc)))

	h.GET("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, csrf.GetToken(ctx))
	})
	h.POST("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, "CSRF token is valid")
	})

	h.Spin()
}
```

The middleware provides `WithErrorFunc` to facilitate user-defined error handling logic. This configuration can be used when users need to have their own error handling logic. When an error occurs after configuration, it will enter the logic of its own configuration.

#### WithKeyLookup

```go
package main

import (
	"context"
	
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/hertz-contrib/csrf"
	"github.com/hertz-contrib/sessions"
	"github.com/hertz-contrib/sessions/cookie"
)

func main() {
	h := server.Default()

	store := cookie.NewStore([]byte("store"))
	h.Use(sessions.New("csrf-session", store))
	h.Use(csrf.New(csrf.WithKeyLookUp("form：csrf")))

	h.GET("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, csrf.GetToken(ctx))
	})
	h.POST("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, "CSRF token is valid")
	})

	h.Spin()
}
```

CSRF middleware provides `WithKeyLookUp` to help users set keyLookup. The middleware will extract the token from the source (supported sources include header, param, query, form). The format is `<source>:<key>`, and the default value is `header:X-CSRF-TOKEN`.

#### WithNext

```go
package main

import (
	"context"
	
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/hertz-contrib/csrf"
	"github.com/hertz-contrib/sessions"
	"github.com/hertz-contrib/sessions/cookie"
)

func isPostMethod(_ context.Context, ctx *app.RequestContext) bool {
	if string(ctx.Method()) == "POST" {
		return true
	} else {
		return false
	}
}

func main() {
	h := server.Default()

	store := cookie.NewStore([]byte("store"))
	h.Use(sessions.New("csrf-session", store))

	//  skip csrf middleware when request method is post
	h.Use(csrf.New(csrf.WithNext(isPostMethod)))

	h.POST("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, "success even no csrf-token in header")
	})
	h.Spin()
}
```

When using this configuration, the use of this middleware can be skipped under certain conditions set by the user.

#### WithExtractor

```go
package main

import (
	"context"
	"errors"
	
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/hertz-contrib/csrf"
	"github.com/hertz-contrib/sessions"
	"github.com/hertz-contrib/sessions/cookie"
)

func myExtractor(c context.Context, ctx *app.RequestContext) (string, error) {
	token ：= ctx.FormValue("csrf-token")
	if token == nil {
		return "", errors.New("missing token in form-data")
	}
	return string(token), nil
}

func main() {
	h := server.Default()

	store := cookie.NewStore([]byte("secret"))
	h.Use(sessions.New("csrf-session", store))
	h.Use(csrf.New(csrf.WithExtractor(myExtractor)))

	h.GET("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, csrf.GetToken(ctx))
	})
	h.POST("/protected", func(c context.Context, ctx *app.RequestContext) {
		ctx.String(200, "CSRF token is valid")
	})

	h.Spin()
}
```

The default Extractor is obtained through KeyLookup, if the user wants to configure other logic is also supported.

## Notes

- This intermediate price needs to be used with sessions middleware, and the underlying logic implementation is highly dependent on sessions.

## Reference

- [cloudwego/hertz](https://github.com/cloudwego/hertz)
- [hertz-contrib/csrf](https://github.com/hertz-contrib/csrf)
- [hertz-contrib/sessions](https://github.com/hertz-contrib/sessions)