Skip to main content

Import

import { csrf } from 'hono/csrf'

Usage

const app = new Hono()

// Default: both origin and sec-fetch-site validation
app.use('*', csrf())

app.post('/protected', (c) => {
  return c.text('Protected action')
})

Options

The csrf middleware accepts an optional CSRFOptions object:
origin
string | string[] | ((origin: string, context: Context) => boolean | Promise<boolean>)
Allowed origins for requests. Can be:
  • A single origin string (e.g., 'https://example.com')
  • An array of allowed origins
  • A function that returns true to allow or false to deny
  • Default: Only same origin as the request URL
secFetchSite
'same-origin' | 'same-site' | 'cross-site' | 'none' | string[] | ((secFetchSite: string, context: Context) => boolean | Promise<boolean>)
Sec-Fetch-Site header validation. Can be:
  • A single allowed value (e.g., 'same-origin')
  • An array of allowed values (e.g., ['same-origin', 'same-site'])
  • A function that returns true to allow or false to deny
  • Default: Only allows 'same-origin'

Signature

csrf(options?: CSRFOptions): MiddlewareHandler

Examples

Basic usage

const app = new Hono()

app.use('*', csrf())

Allow specific origins

app.use('*', csrf({ origin: 'https://example.com' }))

Multiple origins

app.use('*', csrf({ 
  origin: ['https://app.com', 'https://api.com'] 
}))

Allow same-site requests

app.use('*', csrf({ 
  secFetchSite: 'same-origin' 
}))

Allow multiple Sec-Fetch-Site values

app.use('*', csrf({ 
  secFetchSite: ['same-origin', 'same-site'] 
}))

Dynamic Sec-Fetch-Site validation

app.use('*', csrf({
  secFetchSite: (secFetchSite, c) => {
    // Always allow same-origin
    if (secFetchSite === 'same-origin') return true
    
    // Allow cross-site for webhook endpoints
    if (secFetchSite === 'cross-site' && c.req.path.startsWith('/webhook/')) {
      return true
    }
    
    return false
  }
}))

Dynamic origin validation

app.use('*', csrf({
  origin: (origin, c) => {
    // Allow same origin
    if (origin === new URL(c.req.url).origin) return true
    
    // Allow specific trusted domains
    return ['https://app.example.com', 'https://admin.example.com'].includes(origin)
  }
}))

Async validation

app.use('*', csrf({
  origin: async (origin, c) => {
    // Check against database
    const allowed = await db.trustedOrigins.exists(origin)
    return allowed
  }
}))

Behavior

  • Only validates non-safe HTTP methods (POST, PUT, DELETE, PATCH, etc.)
  • Only validates form submissions (Content-Type: application/x-www-form-urlencoded, multipart/form-data, or text/plain)
  • Request is allowed if EITHER validation passes:
    • Sec-Fetch-Site header matches allowed values, OR
    • Origin header matches allowed origins
  • Returns 403 Forbidden if both validations fail
  • Safe methods (GET, HEAD) are always allowed
  • If neither Origin nor Sec-Fetch-Site header is present, request is denied

Build docs developers (and LLMs) love