DEV Community

L2ncE
L2ncE

Posted on

Golang CSRF Defense in Practice

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
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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=
Enter fullscreen mode Exit fullscreen mode
$ curl -X POST 127.0.0.1:8888/protected -H "X-CSRF-TOKEN=UMhM-eqB9CYjeuZO5o-9wJsQhb8KLQUpcRlYQnYagT4="

CSRF token is valid
Enter fullscreen mode Exit fullscreen mode

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 ":" 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

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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

Latest comments (0)