Show table of contents

CSRF

What is a Cross-Site Request Forgery attack and how can you defend your website against it? In this article we're going to implement the double submit cookie pattern in the V programming language to mittigate a CSRF-attack.

[csrf-cross-site-request-forgery.jpg]
Nov 2th security v web

What is CSRF?

A Cross-Site Request Forgery attack, or CSRF attack for short, is an attack in which a malicious website, email, blog, message or program causes the user's web browser to perform an action without the user knowing. Often the user is logged in to the website on which the attack is focused. An attacker can make requests to a website through a CSRF attack as if it were the user themselves. The website targeted by the CSRF attack cannot differentiate between a normal user's request and the attacker's request.

Source code

The code used in this article is from the csrf implementation of vweb. You can find the pull request with all the source code and how to use it in vweb here.

https://github.com

5 steps of a CSRF attack

[A CSRF attack on the website of a bank (mukhaddin-beshkov, 2023)]

A CSRF attack on the website of a bank (mukhaddin-beshkov, 2023)

In this image you can see a 5 step plan for executing a successful CSRF attack on a banking website, note that this example is massively oversimplified.

  1. Victim logs in on a website
  2. The website sends back a session cookie and this cookie gets saved in the browser.
  3. The attacker "forges" a request, this request is mostly in the form of an URL and sends this to the victim. This step usually requires extensive knowledge and recon of the target's website.
  4. If the victim gets tricked by the attacker their browser will request the forged URL. The cookies for the target website get sent automatically with the request.
  5. The target validates the cookie and processes the forged request.

The danger of a CSRF-attack

The actual danger of a CSRF-attack is that the target website couldn't distinguish the forged request from a real request. A successful CSRF-attack gets noticed only when the victim notices the consequences. For authentication-sensitive applications this is a nightmare scenario.

Defences

There are a couple of methods known to defend against a CSRF-attack according to OWASP. Each method has its own pros and cons. But in this article we are going to implement the double submit cookie pattern in the V programming language.

The App

Alright lets code! We are going to make a very basic html profile page where a logged in user can change their name. To test the app you will need to set a session cookie: session_id=1 .

First we need to make a vweb App that will serve as a basis for our app.

main.v

module main

import vweb

struct App {
    vweb.Context
}

pub fn (mut app App) index() vweb.Result {
    return $vweb.html()
}

[post]
pub fn (mut app App) change_name() vweb.Result {
    // check the session id cookie
    session_id := app.get_cookie('session_id') or { '' }
    if session_id != '1' {
        app.set_status(401, '')
        return app.text('HTTP 401: Unauthorized')
    }
    
    name := app.form['name']
    return $vweb.html()
}

fn main() {
    vweb.run(&App{}, 8080)
}

And the corresponding html pages:

templates/index.html

<!DOCTYPE html>
<html>
<head>
    <title>Home</title>
</head>
<body>
    <h1>Profile</h1>
    <p>Update your profile name:</p>
    <form action="/change_name" method="post">
        <input type="text" name="name" placeholder="John Doe"/>
        <input type="submit"/>
    </form>
</body>
</html>

templates/change_name.html

<!DOCTYPE html>
<html>
<head>
    <title>Name</title>
</head>
<body>
    <h1>Hello @name</h1>
</body>
</html>

If you run the app with v run . and visit http://localhost:8080 you will see a simple HTML page. After I fill in my name and submit the form, I can see a page with the text "Hello Casper" .

Exploit the CSRF vulnerability

This app is not protected against CSRF-attacks and we could exploit it by making a fake web page and some javascript. If an attacker would successfully trick a victim into opening that webpage their cookies will be send and their name would be changed without them knowing!

<!DOCTYPE html>
<html>
<head>
    <title>Exploit</title>
</head>
<body>
    <h1>Evil website</h1>
</body>

<script>

const data = new FormData();
data.append("name", "Attacker");
// send a POST request to the url of our website.
fetch("http://localhost:8080/change_name", {
    method: "post",
    body: data
})
    
</script>
</html>

This is ofcourse a gross oversimplification of what is possible with a CSRF-attack.

Adding CSRF protection

The double submit cookie method protects against CSRF because it sets two tokens: one anti-csrf-oken is set as a hidden input in a form and the other is set as a cookie. The token in the cookie is actually the hmac of the anti-csrftoken. The anti-csrftoken contains the session id. This ensures that the token is cryptographically bound to the users session id.

Getting the token

With vweb's CSRF module we can easily get the csrftoken in a route and protect it. See the documentation for the configuration options.

main.v

// add the import and const at the top of your file
import vweb.csrf

const (
    csrf_config := csrf.CsrfConfig{
        // change the secret
        secret: 'my-secret'
        // change to which domains you want to allow
        allowed_hosts: ['*']
        // the name of the session id cookie
        session_cookie: 'session_id'
    }

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

Add this input to the form in templates/index.html

<input type="hidden" name="csrftoken" value="@csrftoken"/>

If you visit the page in your browser a cookie with the name "csrftoken" will be set and the form contains a hidden input with the anti-csrftoken.

Protecting a route

To protect a route simply call csrf.protect at the beginning of the handler.

[post]
pub fn (mut app App) change_name() vweb.Result {
    // protect the route
    csrf.protect(mut app.Context, csrf_config)
    
    // check the session id cookie
    session_id := app.get_cookie('session_id') or { '' }
    if session_id != '1' {
        app.set_status(401, '')
        return app.text('HTTP 401: Unauthorized')
    }
    
    name := app.form['name']
    return $vweb.html()
}

If you make a post request to /change_name without any cookies we now get an http 401 response! You will get the same response if your cookies are valid, but there is no token present in the form data.

Other usages

Middleware

You can also use vweb's middleware to protect multiple routes at once.

main.v

// The rest is the same

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

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

[post]
pub fn (mut app App) change_name() vweb.Result {
    // protect the route
    csrf.protect(mut app.Context, csrf_config)
    
    // check the session id cookie
    session_id := app.get_cookie('session_id') or { '' }
    if session_id != '1' {
        app.set_status(401, '')
        return app.text('HTTP 401: Unauthorized')
    }
    
    name := app.form['name']
    return $vweb.html()
}

CsrfApp

Or you can use the CsrfApp struct if you want the protection to be available inside your App struct.

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-secret'
            // change to which domains you want to allow
            allowed_hosts: ['*']
            // the name of the session id cookie
            session_cookie: 'session_id'
		}
	}
	vweb.run(app, 8080)
}

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

[post]
pub fn (mut app App) change_name() vweb.Result {
    // protect the route
    app.csrf.protect(mut app.Context)
    
    // check the session id cookie
    session_id := app.get_cookie('session_id') or { '' }
    if session_id != '1' {
        app.set_status(401, '')
        return app.text('HTTP 401: Unauthorized')
    }
    
    name := app.form['name']
    return $vweb.html()
}

Source Code

See vlib/vweb/csrf for the source code.