Skip to content

vweb.csrf #

Cross-Site Request Forgery (CSRF) protection

This module implements the [double submit cookie][owasp] technique to protect routes from CSRF attacks.

CSRF is a type of attack that occurs when a malicious program/website (and others) causes a user's web browser to perform an action without them knowing. A web browser automatically sends cookies to a website when it performs a request, including session cookies. So if a user is authenticated on your website the website can not distinguish a forged request by a legitimate request.

When to not add CSRF-protection

If you are creating a service that is intended to be used by other servers e.g. an API, you probably don't want CSRF-protection. An alternative would be to send an Authorization token in, and only in, an HTTP-header (like JSON Web Tokens). If you do that your website isn't vulnerable to CSRF-attacks.

Usage

You can add CsrfApp to your own App struct to have the functions available in your app's context, or you can use it with the middleware of vweb.

The advantage of the middleware approach is that you have to define the configuration separate from your App. This makes it possible to share the configuration between modules or controllers.

Usage with the CsrfApp

Change secret and allowed_hosts when creating the CsrfApp.

Example:

module main

import net.http
import vweb
import vweb.csrf

struct App {
    vweb.Context
pub mut:
    csrf csrf.CsrfApp [vweb_global]
}

fn main() {
    app := &App{
        csrf: csrf.CsrfApp{
            // change the secret
            secret: 'my-64bytes-secret'
            // change to which domains you want to allow
            allowed_hosts: ['*']
        }
    }
    vweb.run(app, 8080)
}

pub fn (mut app App) index() vweb.Result {
    // this line sets `app.token` and the cookie
    app.csrf.set_token(mut app.Context)
    return $vweb.html()
}

[post]
pub fn (mut app App) auth() vweb.Result {
    // this line protects the route against CSRF
    app.csrf.protect(mut app.Context)
    return app.text('authenticated!')
}

index.html

<form action='/auth' method='post'>
    <input type='hidden' name='@app.csrf.token_name' value='@app.csrf.token'/>
    <label for='password'>Your password:</label>
    <input type='text' id='password' name='password' placeholder='Your password' />
</form>

Usage without CsrfApp

If you use vweb.Middleware you can protect multiple routes at once.

Example:

module main

import net.http
import vweb
import vweb.csrf

const (
    // the configuration moved here
    csrf_config = csrf.CsrfConfig{
        // change the secret
        secret: 'my-64bytes-secret'
        // change to which domains you want to allow
        allowed_hosts: ['*']
    }
)

struct App {
    vweb.Context
pub mut:
    middlewares map[string][]vweb.Middleware
}

fn main() {
    app := &App{
        middlewares: {
            // protect all routes starting with the url '/auth'
            '/auth': [csrf.middleware(csrf_config)]
        }
    }
    vweb.run(app, 8080)
}

pub fn (mut app App) index() vweb.Result {
    // get the token and set the cookie
    csrftoken := csrf.set_token(mut app.Context, csrf_config)
    return $vweb.html()
}

[post]
pub fn (mut app App) auth() vweb.Result {
    return app.text('authenticated!')
}

[post]
pub fn (mut app App) register() vweb.Result {
    // protect an individual route with the following line
    csrf.protect(mut app.Context, csrf_config)
    // ...
}

index.html (the hidden input has changed)

<form action='/auth' method='post'>
    <input type='hidden' name='@csrf_config.token_name' value='@csrftoken'/>
    <label for='password'>Your password:</label>
    <input type='text' id='password' name='password' placeholder='Your password' />
</form>

Protect all routes

It is possible to protect all routes against CSRF-attacks. Every request that is not defined as a safe method (GET, OPTIONS, HEAD by default) will have CSRF-protection.

Example:

pub fn (mut app App) before_request() {
    app.csrf.protect(mut app.Context)
    // or if you don't use `CsrfApp`:
    // csrf.protect(mut app.Context, csrf_config)
}

How it works

This module implements the [double submit cookie][owasp] technique: a random token is generated, the CSRF-token. The hmac of this token and the secret key is stored in a cookie.

When a request is made, the CSRF-token should be placed inside a HTML form element. The CSRF-token the hmac of the CSRF-token in the formdata is compared to the cookie. If the values match, the request is accepted.

This approach has the advantage of being stateless: there is no need to store tokens on the server side and validate them. The token and cookie are bound cryptographically to each other so an attacker would need to know both values in order to make a CSRF-attack succeed. That is why is it important to not leak the CSRF-token via an url, or some other way. See [client side CSRF][client-side-csrf] for more information.

This is a high level overview of the implementation.

Security Considerations

The secret key

The secret key should be a random string that is not easily guessable. The recommended size is 64 bytes.

Sessions

If your app supports some kind of user sessions, it is recommended to cryptographically bind the CSRF-token to the users' session. You can do that by providing the name of the session ID cookie. If an attacker changes the session ID in the cookie, in the token or both the hmac will be different and the request will be rejected.

Example:

csrf_config = csrf.CsrfConfig{
    // ...
    session_cookie: 'my_session_id_cookie_name'
}

Safe Methods

The HTTP methods GET, OPTIONS, HEAD are considered [safe methods][mozilla-safe-methods] meaning they should not alter the state of an application. If a request with a "safe method" is made, the csrf protection will be skipped.

You can change which methods are considered safe by changing CsrfConfig.safe_methods.

Allowed Hosts

By default, both the http Origin and Referer headers are checked and matched strictly to the values in allowed_hosts. That means that you need to include each subdomain.

If the value of allowed_hosts contains the wildcard: '*' the headers will not be checked.

Domain name matching

The following configuration will not allow requests made from test.example.com, only from example.com.

Example

config := csrf.CsrfConfig{
    secret: '...'
    allowed_hosts: ['example.com']
}

Referer, Origin header check

In some cases (like if your server is behind a proxy), the Origin or Referer header will not be present. If that is your case you can set check_origin_and_referer to false. Request will now be accepted when the Origin or Referer header is valid.

Share csrf cookie with subdomains

If you need to share the CSRF-token cookie with subdomains, you can set same_site to .same_site_lax_mode.

Configuration

All configuration options are defined in CsrfConfig.

[//]: # (Sources) [owasp]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie [client-side-csrf]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#client-side-csrf [mozilla-safe-methods]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP

fn middleware #

fn middleware(config &CsrfConfig) vweb.Middleware

middleware returns a function that you can use in app.middlewares

fn protect #

fn protect(mut ctx vweb.Context, config &CsrfConfig) bool

protect returns false and sends an http 401 response when the csrf verification fails. protect will always return true if the current request method is in config.safe_methods.

fn set_token #

fn set_token(mut ctx vweb.Context, config &CsrfConfig) string

set_token returns the csrftoken and sets an encrypted cookie with the hmac of config.get_secret and the csrftoken

struct CsrfApp #

struct CsrfApp {
	CsrfConfig
pub mut:
	// the csrftoken that should be placed in an html form
	token string
}

fn (CsrfApp) set_token #

fn (mut app CsrfApp) set_token(mut ctx vweb.Context)

set_token is the app wrapper for set_token

fn (CsrfApp) protect #

fn (mut app CsrfApp) protect(mut ctx vweb.Context) bool

protect is the app wrapper for protect

struct CsrfConfig #

@[params]
struct CsrfConfig {
pub:
	secret string
	// how long the random part of the csrf-token should be
	nonce_length int = 64
	// HTTP "safe" methods meaning they shouldn't alter state.
	// If a request with any of these methods is made, `protect` will always return true
	// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
	safe_methods []http.Method = [.get, .head, .options]
	// which hosts are allowed, enforced by checking the Origin and Referer header
	// if allowed_hosts contains '*' the check will be skipped.
	// Subdomains need to be included separately: a request from `"sub.example.com"`
	//  will be rejected when `allowed_host = ['example.com']`.
	allowed_hosts []string
	// if set to true both the Referer and Origin headers must match `allowed_hosts`
	// else if either one is valid the request is accepted
	check_origin_and_referer bool = true
	// the name of the csrf-token in the hidden html input
	token_name string = 'csrftoken'
	// the name of the cookie that contains the session id
	session_cookie string
	// cookie options
	cookie_name string        = 'csrftoken'
	same_site   http.SameSite = .same_site_strict_mode
	cookie_path string        = '/'
	// how long the cookie stays valid in seconds. Default is 30 days
	max_age       int = 60 * 60 * 24 * 30
	cookie_domain string
}