Schematra-CSRF

Cross-Site Request Forgery (CSRF) protection middleware for Schematra web applications.

Description

Schematra-CSRF provides CSRF protection for Schematra applications through middleware that automatically validates tokens on unsafe HTTP methods. It generates random tokens, stores them in sessions, and validates them on form submissions and API requests to help prevent CSRF attacks.

Requirements

Installation

chicken-install schematra-csrf

Basic Usage

(import schematra schematra-csrf schematra-session chiccup)

;; Enable sessions (required for CSRF)
(use-middleware! (session-middleware "secret-key"))

;; Enable CSRF protection
(use-middleware! (csrf-middleware))

;; Form with CSRF protection
(get ("/form")
     (ccup->html
      `[form (@ (method "POST") (action "/submit"))
        ,(chiccup-csrf-hidden-input)  ; Automatic hidden input
        [input (@ (type "text") (name "message"))]
        [button "Submit"]]))

(post ("/submit")
      "Form processed securely!")

(schematra-install)
(schematra-start)

API

Middleware

[procedure] (csrf-middleware)

Creates CSRF protection middleware for Schematra applications. This middleware automatically protects against Cross-Site Request Forgery attacks by validating CSRF tokens on unsafe HTTP methods.

Behavior:

The middleware looks for CSRF tokens in two places:

  1. Form data using the field name from csrf-form-field parameter
  2. HTTP header X-CSRF-Token
;; Enable CSRF protection globally
(use-middleware! (csrf-middleware))

;; Custom configuration
(csrf-form-field "authenticity_token")
(csrf-token-key "my-csrf-key")
(use-middleware! (csrf-middleware))

Token Management

[procedure] (csrf-get-token)

Retrieves or generates a CSRF token for the current session. This function first checks if a CSRF token already exists in the session. If found, it returns the existing token. If not found, it generates a new cryptographically secure token, stores it in the session, and returns it.

Returns: A base64-encoded string containing the CSRF token

;; Get token for use in forms
(let ((token (csrf-get-token)))
  `[input (@ (type "hidden")
             (name ,(symbol->string (csrf-form-field)))
             (value ,token))])

;; Get token for AJAX requests
(get ("/api/token")
     (csrf-get-token))
[procedure] (chiccup-csrf-hidden-input)

Generates a Chiccup-formatted hidden input element containing the CSRF token. This is a convenience function that combines token generation with HTML generation.

Returns: A Chiccup list representing an HTML hidden input element

;; Use in forms
`[form (@ (method "POST") (action "/submit"))
  ,(chiccup-csrf-hidden-input)  ; Automatic CSRF protection
  [input (@ (type "text") (name "username"))]
  [button "Submit"]]

;; Equivalent manual version
`[form (@ (method "POST") (action "/submit"))
  [input (@ (type "hidden")
            (name ,(symbol->string (csrf-form-field)))
            (value ,(csrf-get-token)))]
  [input (@ (type "text") (name "username"))]
  [button "Submit"]]

Configuration Parameters

[parameter] (csrf-token-key [key])

Parameter that defines the session key used to store CSRF tokens.

Parameters:

;; Use default key
(csrf-token-key) ; => "csrf-token"

;; Customize the session key
(csrf-token-key "my-csrf-key")
[parameter] (csrf-form-field [field-name])

Parameter that defines the HTML form field name used for CSRF tokens.

Parameters:

;; Use default field name
(csrf-form-field) ; => '_csrf_token'

;; Customize the form field name
(csrf-form-field 'authenticity_token)

AJAX Integration

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

// Get token from a dedicated endpoint
fetch('/api/csrf-token')
  .then(response => response.text())
  .then(token => {
    // Use token in subsequent requests
    fetch('/api/data', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': token
      },
      body: JSON.stringify({data: 'example'})
    });
  });

Provide a token endpoint:

(get ("/api/csrf-token")
     (csrf-get-token))

Complete Example

(import schematra schematra-csrf schematra-session chiccup)

;; Configure CSRF (optional - uses sensible defaults)
(csrf-form-field 'authenticity_token)
(csrf-token-key "app-csrf-token")

;; Install middleware (order matters!)
(use-middleware! (session-middleware "your-secret-key"))
(use-middleware! (csrf-middleware))

;; Home page with protected form
(get ("/")
     (ccup->html
      `[html
        [head [title "CSRF Demo"]]
        [body
         [h1 "CSRF Protected Form"]
         [form (@ (method "POST") (action "/submit"))
          ,(chiccup-csrf-hidden-input)
          [p [label "Message: "]
             [input (@ (type "text") (name "message") (required))]]
          [p [button (@ (type "submit")) "Send Message"]]]
         
         [h2 "AJAX Example"]
         [button (@ (onclick "sendAjaxRequest()")) "Send AJAX"]
         
         [script "
         function sendAjaxRequest() {
           fetch('/api/csrf-token')
             .then(response => response.text())
             .then(token => {
               return fetch('/api/submit', {
                 method: 'POST',
                 headers: {
                   'Content-Type': 'application/json',
                   'X-CSRF-Token': token
                 },
                 body: JSON.stringify({message: 'Hello from AJAX'})
               });
             })
             .then(response => response.text())
             .then(data => alert('Response: ' + data));
         }"]]]))

;; Form submission handler
(post ("/submit")
      (let ((message (alist-ref 'message (current-params))))
        (ccup->html
         `[html
           [body 
            [h1 "Message Received"]
            [p ,(string-append "You said: " (or message ""))]
            [a (@ (href "/")) "Back"]]])))

;; CSRF token endpoint for AJAX
(get ("/api/csrf-token")
     (csrf-get-token))

;; AJAX submission handler
(post ("/api/submit")
      (let ((message (alist-ref 'message (current-params))))
        (string-append "AJAX message received: " (or message ""))))

(schematra-install)
(schematra-start)

Security Notes

License

Copyright © 2025 Rolando Abarca. Released under the BSD-3-Clause license.

Repository

Part of the Schematra project.