CSRF

What is CSRF?

Cross-site request forgery (also known as CSRF) is a web security vulnerability that allows an attacker to induce users to perform actions that they do not intend to perform. It allows an attacker to partly circumvent the same origin policy, which is designed to prevent different websites from interfering with each other.

Preventing CSRF attacks

The most robust way to defend against CSRF attacks is to include a CSRF token within relevant requests. The token should be:

  • Unpredictable with high entropy, as for session tokens in general.

  • Tied to the user's session.

  • Strictly validated in every case before the relevant action is executed.

Find CSRF vulnerabilities using Burp Suite's web vulnerability scanner An additional defense that is partially effective against CSRF, and can be used in conjunction with CSRF tokens, is SameSite cookies.

Read more at: https://portswigger.net/web-security/csrf

CSRF Protection

Iris makes it easy to protect your application from cross-site request forgery (CSRF) attacks. You'll learn how to use the Iris CSRF middleware to generate csrf token and protect your APIs.

Install

The CSRF middleware's source code is located at the iris-contrib/middleware repository.

go get github.com/iris-contrib/middleware/csrf@master

CSRF is easy to use: add the middleware to your router with the below:

app := iris.New()
// [...]
CSRF := csrf.Protect([]byte("32-byte-long-auth-key"))
app.Use(CSRF)

...and then collect the token with csrf.Token(ctx) in your handlers before passing it to the template, JSON body or HTTP header (see below).

Note that the authentication key passed to csrf.Protect([]byte(key)) should be 32-bytes long and persist across application restarts. Generating a random key won't allow you to authenticate existing cookies and will break your CSRF validation.

CSRF inspects the HTTP headers (first) and form body (second) on subsequent POST/PUT/PATCH/DELETE/etc. requests for the token.

Excluding Routes From CSRF Protection

Sometimes you may wish to exclude some routes from CSRF protection under a group. There are two available ways to do this. Read below...

1. Using the *Route.RemoveHandler method:

app.Post("/unprotected", unprotected).RemoveHandler(CSRF)

2. The UnsafeSkipCheck will skip the CSRF check for any requests. This must be called before the CSRF middleware:

Note: You should not set this without otherwise securing the request from CSRF attacks. The primary use-case for this function is to turn off CSRF checks for non-browser clients using authorization tokens against your API.

app.Use(func(ctx iris.Context) {
    shouldSkipCSRF = [custom condition...]
    if shouldSkipCSRF {
        csrf.UnsafeSkipCheck(ctx)
    }
    ctx.Next()
})
app.Use(CSRF)
app.Use(otherMiddlewares...)

HTML Forms

Here's the common use-case: HTML forms you want to provide CSRF protection for, in order to protect malicious POST requests being made:

package main

import (
	"github.com/kataras/iris/v12"

	"github.com/iris-contrib/middleware/csrf"
)

func main() {
	app := iris.New()
	app.RegisterView(iris.HTML("./views", ".html"))
    // All POST requests without a valid token will
    // return HTTP 403 Forbidden.
    // We should also ensure that our mutating
    // (non-idempotent) handler only matches on POST requests.
    // We can check that here, at the router level,
    // or within the handler itself via r.Method.
    userAPI := app.Party("/user")
    CSRF := csrf.Protect(
        // Note that the authentication key
        // provided should be 32 bytes
        // long and persist across application restarts.
        []byte("9AB0F421E53A477C084477AEA06096F5"),
        // WARNING: Set it to true on production with HTTPS.
        csrf.Secure(false),
    )
    // Register the middleware to this Party's matched routes.
    userAPI.Use(CSRF)
    userAPI.Post("/signup", SubmitSignupForm)
    userAPI.Get("/signup", ShowSignupForm)

    app.Listen(":8080")
}

func ShowSignupForm(ctx iris.Context) {
    // user/signup.html just needs a {{ .csrfField }} template tag for
    // csrf.TemplateField to inject the CSRF token into.
    ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx))
    ctx.View("user/signup.html")
    // We could also retrieve the token directly from csrf.Token(ctx) and
    // set it in the request header - ctx.Header("X-CSRF-Token", token)
    // This is useful if you're sending JSON to clients
    // or a front-end JavaScript framework.
}

func SubmitSignupForm(ctx iris.Context) {
    // We can trust that requests making it this far have satisfied
    // our CSRF protection requirements.
}

Note that the CSRF middleware will (by necessity) consume the request body if the token is passed via POST form values. If you need to consume this in your handler, insert your own middleware earlier in the chain to capture the request body.

JavaScript Applications

This approach is useful if you're using a front-end JavaScript framework like React, Ember or Angular, and are providing a JSON API. Specifically, we need to provide a way for our front-end fetch/AJAX calls to pass the token on each fetch (AJAX/XMLHttpRequest) request. We achieve this by:

  • Parsing the token from the <input> field generated by the csrf.TemplateField(ctx) helper, or passing it back in a response header.

  • Sending this token back on every request

  • Ensuring our cookie is attached to the request so that the form/header value can be compared to the cookie value.

func GetUser(ctx iris.Context) {
    // Authenticate the request, get the id from the route params,
    // and fetch the user from the DB, etc.

    // Get the token and pass it in the CSRF header.
    // Our JSON-speaking client or JavaScript framework can now read
    // the header and return the token in
    // in its own "X-CSRF-Token" request header on the subsequent POST.
    ctx.Header("X-CSRF-Token", csrf.Token(ctx))
    ctx.JSON(user)
}

In our JavaScript application, we should read the token from the response headers and pass it in a request header for all requests. Here's what that looks like when using Axios, a popular JavaScript HTTP client library:

// You can alternatively parse the response header for the X-CSRF-Token,
// and store that instead, if you followed the steps above
// to write the token to a response header.
let csrfToken = document.getElementsByName("csrf.token")[0].value

// via https://github.com/axios/axios#creating-an-instance
const instance = axios.create({
  baseURL: "https://example.com/api/",
  timeout: 1000,
  headers: { "X-CSRF-Token": csrfToken }
})

// Now, any HTTP request you make will include
// the csrfToken from the page, provided you update the
// csrfToken variable for each render.
try {
  let resp = await instance.post(endpoint, formData)
  // Do something with resp
} catch (err) {
  // Handle the exception
}

If you plan to host your JavaScript application on another domain, you can use the Trusted Origins feature to allow the host of your JavaScript application to make requests to your Go application. Observe the code snippet below:

CSRF := csrf.New(csrf.Options{
    TrustedOrigins: []string{"ui.domain.com"},
    Store: csrf.NewCookieStore(
        []byte("9AB0F421E53A477C084477AEA06096F5"),
        csrf.Secure(false)),
}).Protect

On the example above, you're authorizing requests from ui.domain.com to make valid CSRF requests to your application, so you can have your API server on another domain without problems.

Setting SameSite

Go 1.11 introduced the option to set the SameSite attribute in cookies. This is valuable if a developer wants to instruct a browser to not include cookies during a cross site request. SameSiteStrictMode prevents all cross site requests from including the cookie. SameSiteLaxMode prevents CSRF prone requests (POST) from including the cookie but allows the cookie to be included in GET requests to support external linking.

import (
    "net/http"

	"github.com/kataras/iris/v12"
	"github.com/iris-contrib/middleware/csrf"
)

func main() {
    CSRF := csrf.Protect(
      []byte("a-32-byte-long-key-goes-here"),
      // instruct the browser to never send cookies
      // during cross site requests
      csrf.SameSite(http.SameSiteStrictMode),
    )

    // [...]
}

Setting Options

What about providing your own error handler or store and changing the HTTP header the package inspects on requests? (i.e. an existing API you're porting to Go). Well, CSRF provides options for changing these as you see fit:

CSRF := csrf.New(csrf.Options{
    TrustedOrigins: []string{"ui.domain.com"},
    RequestHeader: "X-CSRF-Token",
    FieldName:     "csrf.token",
    ErrorHandler:  csrf.UnauthorizedHandler,
    Store: csrf.NewCookieStore(
        []byte("9AB0F421E53A477C084477AEA06096F5"), csrf.Secure(false)),
})

The CSRF.Filter method.

func access(ctx iris.Context) {
    if CSRF.Filter(ctx) {
        // Succeed.
    } else {
        // It's a failure.
        // Customize the error response, the ErrorHandler is not called.
    }
}

OR use the iris.NewConditionalHandler:

app.Get(iris.NewConditionalHandler(CSRF.Filter, protectedHandler))

The main CSRF.Protect method (same as csrf.Protect package-level function as we've seen in the previous examples).

app.Use(CSRF.Protect)

Last updated