ruby erb 76 lines · 3 tabs

CSRF Protection for Sinatra Forms with Rack::Protection AuthenticityToken

Shared by codesnips Sep 2026
3 tabs
require 'sinatra/base'
require 'rack/protection'

class TransferApp < Sinatra::Base
  enable :sessions
  set :session_secret, ENV.fetch('SESSION_SECRET', 'a' * 64)

  # Only guards unsafe verbs (POST/PUT/PATCH/DELETE); GETs pass through.
  use Rack::Protection::AuthenticityToken

  helpers do
    def csrf_token
      env['rack.session'][:csrf]
    end

    def csrf_tag
      %(<input type="hidden" name="authenticity_token" value="#{csrf_token}">)
    end
  end

  get '/transfer' do
    erb :transfer
  end

  post '/transfer' do
    # Reaches here only if the middleware validated the token.
    amount = params[:amount].to_i
    to = params[:to].to_s
    "Transferred #{amount} to #{to}"
  end

  run! if app_file == $0
end
3 files · ruby, erb Explain with highlit

This snippet shows how a classic Sinatra application defends state-changing form submissions against cross-site request forgery (CSRF) using Rack::Protection::AuthenticityToken. CSRF is an attack where a malicious page tricks a logged-in user's browser into submitting a request to another site using the ambient session cookie. The standard defense is the synchronizer token pattern: the server embeds an unpredictable per-session token in every form, and rejects any unsafe request whose token is missing or wrong.

In app.rb, session support is enabled with enable :sessions and a session_secret, which is a hard prerequisite — the token is derived from the session, so without a signed session cookie there is nothing to bind the token to. The app deliberately activates Rack::Protection::AuthenticityToken on its own rather than pulling in the whole Rack::Protection bundle, keeping the middleware stack explicit. A helpers block exposes csrf_token, which reads the value the middleware stashes in env['rack.session'][:csrf], and csrf_tag, which renders a ready-to-use hidden input. The POST /transfer route is the protected action; by the time its block runs, the middleware has already validated the incoming token, so the handler can assume the request is legitimate.

A subtle but important detail is that Rack::Protection::AuthenticityToken only guards unsafe HTTP verbs — POST, PUT, PATCH, DELETE. GET requests pass through untouched, which is correct because safe methods must never mutate state. This is also why the transfer must be a POST and not a GET.

The transfer.erb view demonstrates the client side: csrf_tag injects the hidden field, so an ordinary browser form submission carries the token automatically. Any forged cross-origin form lacks the value and is rejected with a 403 before the route ever executes.

The spec/csrf_spec.rb tests lock in both halves of the contract — a POST without a token is refused, and a POST echoing the session's token succeeds. A common pitfall worth noting is AJAX: fetch/XHR requests must send the token via a header or body field explicitly, since there is no form to carry it. Reaching for this pattern is appropriate for any server-rendered Sinatra app that mutates data behind a session.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
bash
#!/usr/bin/env bash
set -euo pipefail

export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"

Secrets management with environment isolation and Vault

secrets-management vault environment-variables
by Kai Nakamura 1 tab
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

CSRF Protection for Sinatra Forms with Rack::Protection AuthenticityToken — share card
Link copied