Golang CSRF Defense in Practice
L2ncE
Posted on December 4, 2022
Hertz
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.
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 extension provided by the community.
$ go get github.com/hertz-contrib/csrf
Basic use
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
$ curl 127.0.0.1:8888/protected
UMhM-eqB9CYjeuZO5o-9wJsQhb8KLQUpcRlYQnYagT4=
$ 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 " |
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
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
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
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
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
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
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
Posted on December 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.