csrf

package
v1.29.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 31, 2025 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package csrf provides Cross-Site Request Forgery (CSRF) protection middleware for HTTP handlers. It generates cryptographically secure tokens for safe methods (GET, HEAD, OPTIONS) and validates them for state-changing methods (POST, PUT, DELETE, PATCH).

Usage:

store := csrf.NewTokenStore()
handler := store.Middleware(yourHandler)
http.ListenAndServe(":8080", handler)

In HTML forms, include the CSRF token as a hidden field:

<form method="POST">
    <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
    <!-- other fields -->
</form>

For AJAX requests, include the token in the X-CSRF-Token header:

fetch('/api/endpoint', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': getCookieValue('csrf_token')
    }
})

Package csrf provides Cross-Site Request Forgery (CSRF) protection for web applications.

CSRF attacks occur when a malicious website causes a user's browser to perform unwanted actions on a trusted site where the user is authenticated. This package prevents such attacks by requiring a cryptographically secure token to be included with all state-changing requests.

Basic Usage

Create a token store and wrap your HTTP handler with the CSRF middleware:

import "github.com/patdeg/common/csrf"

func main() {
	store := csrf.NewTokenStore()
	mux := http.NewServeMux()
	mux.HandleFunc("/", homeHandler)

	// Wrap with CSRF protection
	handler := store.Middleware(mux)

	http.ListenAndServe(":8080", handler)
}

How It Works

The middleware operates differently based on the HTTP method:

Safe methods (GET, HEAD, OPTIONS):

  • Generate a new CSRF token
  • Set it in a cookie named "csrf_token"
  • Allow the request to proceed

State-changing methods (POST, PUT, DELETE, PATCH):

  • Require a valid CSRF token
  • Check both the cookie and the request (header or form field)
  • Validate the token matches and hasn't expired
  • Return 403 Forbidden if validation fails

HTML Forms

Include the CSRF token as a hidden input field:

<form method="POST" action="/submit">
    <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
    <input type="text" name="username">
    <button type="submit">Submit</button>
</form>

In your handler, pass the token to the template:

func homeHandler(w http.ResponseWriter, r *http.Request) {
    data := struct {
        CSRFToken string
    }{
        CSRFToken: csrf.GetToken(r),
    }
    tmpl.Execute(w, data)
}

Or use the common package helper:

import "github.com/patdeg/common"

data := struct {
    CSRFToken string
}{
    CSRFToken: common.GetCSRFToken(r),
}

AJAX Requests

For AJAX requests, include the token in the X-CSRF-Token header:

// Vanilla JavaScript
const token = getCookie('csrf_token');

fetch('/api/endpoint', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': token
    },
    body: JSON.stringify(data)
});

function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

HTMX Integration

For HTMX, add the token to all requests automatically:

<script>
document.body.addEventListener('htmx:configRequest', function(evt) {
    const token = document.cookie
        .split('; ')
        .find(row => row.startsWith('csrf_token='))
        ?.split('=')[1];

    if (token) {
        evt.detail.headers['X-CSRF-Token'] = token;
    }
});
</script>

jQuery Integration

$.ajaxSetup({
    beforeSend: function(xhr) {
        const token = getCookie('csrf_token');
        if (token) {
            xhr.setRequestHeader('X-CSRF-Token', token);
        }
    }
});

Token Lifecycle

  • Tokens are generated with 256 bits of cryptographic randomness
  • Tokens expire after 24 hours
  • Expired tokens are automatically cleaned up every hour
  • Each token is validated using constant-time comparison to prevent timing attacks

Security Considerations

  • The CSRF cookie has HttpOnly=false so JavaScript can read it for AJAX requests
  • The cookie uses SameSite=Strict for additional protection
  • The cookie is Secure (HTTPS only) except on localhost for development
  • Token validation uses constant-time comparison to prevent timing attacks
  • Tokens are cryptographically random (crypto/rand, not math/rand)

The CSRF token cookie is set with the following attributes:

  • Name: csrf_token
  • Path: /
  • MaxAge: 86400 (24 hours)
  • HttpOnly: false (JavaScript needs to read this)
  • Secure: true (except localhost)
  • SameSite: Strict

Error Responses

The middleware returns HTTP 403 Forbidden with descriptive messages:

  • "CSRF token cookie missing" - No csrf_token cookie in request
  • "CSRF token missing from request" - No token in header or form
  • "CSRF token invalid or expired" - Token doesn't exist or has expired
  • "CSRF token validation failed" - Cookie and request tokens don't match

Testing

When writing tests, you can either:

1. Make a GET request first to obtain a token:

// Get token
getReq := httptest.NewRequest("GET", "/", nil)
getW := httptest.NewRecorder()
handler.ServeHTTP(getW, getReq)

var token string
for _, c := range getW.Result().Cookies() {
    if c.Name == "csrf_token" {
        token = c.Value
        break
    }
}

// Use token in POST
postReq := httptest.NewRequest("POST", "/", nil)
postReq.Header.Set("X-CSRF-Token", token)
postReq.AddCookie(&http.Cookie{Name: "csrf_token", Value: token})

2. Generate a token directly:

store := csrf.NewTokenStore()
token, _ := store.GenerateToken()

req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("X-CSRF-Token", token)
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: token})

Complete Example

package main

import (
    "html/template"
    "net/http"

    "github.com/patdeg/common/csrf"
)

var tmpl = template.Must(template.New("form").Parse(`
<!DOCTYPE html>
<html>
<head><title>CSRF Example</title></head>
<body>
    <form method="POST" action="/submit">
        <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
        <input type="text" name="username" placeholder="Username">
        <button type="submit">Submit</button>
    </form>
</body>
</html>
`))

func main() {
    store := csrf.NewTokenStore()

    mux := http.NewServeMux()
    mux.HandleFunc("/", formHandler)
    mux.HandleFunc("/submit", submitHandler)

    handler := store.Middleware(mux)

    http.ListenAndServe(":8080", handler)
}

func formHandler(w http.ResponseWriter, r *http.Request) {
    data := struct {
        CSRFToken string
    }{
        CSRFToken: csrf.GetToken(r),
    }
    tmpl.Execute(w, data)
}

func submitHandler(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    w.Write([]byte("Hello, " + username))
}

Performance

The CSRF middleware is lightweight and adds minimal overhead:

  • Token generation: ~100-200 ns/op (uses crypto/rand)
  • Token validation: ~50-100 ns/op (map lookup + constant-time compare)
  • Middleware overhead: ~500-1000 ns/op for GET requests

The token store uses a mutex-protected map, which scales well for most applications. For high-traffic scenarios with millions of concurrent users, consider implementing a distributed token store using Redis or Memcache.

Thread Safety

All operations in this package are thread-safe:

  • TokenStore uses sync.RWMutex for concurrent access
  • Token generation uses crypto/rand which is thread-safe
  • Middleware can be called concurrently from multiple goroutines

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetToken

func GetToken(r *http.Request) string

GetToken retrieves the CSRF token from the request cookie This helper function is useful for injecting the token into templates Returns empty string if the cookie is not present

Types

type TokenStore

type TokenStore struct {
	// contains filtered or unexported fields
}

TokenStore manages CSRF tokens with automatic expiry and cleanup

func NewTokenStore

func NewTokenStore() *TokenStore

NewTokenStore creates a new token store and starts a background cleanup goroutine that removes expired tokens every hour

func (*TokenStore) GenerateToken

func (ts *TokenStore) GenerateToken() (string, error)

GenerateToken creates a cryptographically secure random token Returns the base64-encoded token string or an error if random generation fails

func (*TokenStore) Middleware

func (ts *TokenStore) Middleware(next http.Handler) http.Handler

Middleware provides CSRF protection for HTTP handlers Safe methods (GET, HEAD, OPTIONS) generate and set a new token State-changing methods (POST, PUT, DELETE, PATCH) validate the token

func (*TokenStore) ValidateToken

func (ts *TokenStore) ValidateToken(token string) bool

ValidateToken checks if a token is valid and not expired Returns true if the token exists and has not expired

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL