bx-csrf: CSRF Protection
Installation
install-bx-module bx-csrf
# CommandBox
box install bx-csrf
BIFs
| BIF | Description |
|---|---|
CSRFGenerateToken( [key], [forceNew] ) |
Generate a CSRF token (optionally scoped to a key) |
CSRFVerifyToken( token, [key] ) |
Verify a CSRF token (returns boolean) |
CSRFHiddenField( [key], [forceNew] ) |
Generate a ready-to-embed <input type="hidden"> HTML field |
CSRFRotate() |
Rotate/invalidate all current tokens, forcing regeneration |
Basic Form Protection
<!-- In your form template -->
<form method="POST" action="/submit">
<!-- Embed the token as a hidden field -->
<bx:output>#CSRFHiddenField()#</bx:output>
<!-- Renders: <input type="hidden" name="csrf" value="...token..."> -->
<input type="text" name="email" />
<button type="submit">Submit</button>
</form>
// In your form handler
function processForm() {
// Verify the submitted token
if ( !CSRFVerifyToken( form.csrf ) ) {
throw( type: "Security.CSRFTokenInvalid", message: "Invalid CSRF token" )
}
// Safe to process form data
// ...
}
Scoped Tokens (Per-Form)
Use scoped tokens when you have multiple forms on the same page or need per-action tokens:
// Generate a scoped token (named "deleteUser")
token = CSRFGenerateToken( "deleteUser" )
// Verify the scoped token
if ( !CSRFVerifyToken( form.csrf, "deleteUser" ) ) {
throw( type: "Security.CSRFTokenInvalid", message: "Invalid delete token" )
}
<!-- Scoped hidden field -->
<bx:output>#CSRFHiddenField( "deleteUser" )#</bx:output>
AJAX / API Requests (Header-Based)
// Client-side: include token in X-CSRF-Token header
// fetch("/api/update", {
// method: "POST",
// headers: { "X-CSRF-Token": csrfToken },
// body: JSON.stringify(data)
// })
// Server-side: auto-verification (configure in boxlang.json)
// Or manually:
if ( !CSRFVerifyToken( getHTTPRequestData().headers[ "X-CSRF-Token" ] ?: "" ) ) {
throw( type: "Security.CSRFTokenInvalid", message: "Missing or invalid CSRF token" )
}
Token Rotation
// Rotate all tokens (e.g., after login/logout)
CSRFRotate()
// All existing tokens are immediately invalidated
// Next call to CSRFGenerateToken() creates a fresh token
Configuration (boxlang.json)
{
"modules": {
"csrf": {
"settings": {
"cacheStorage" : "session", // Cache for token storage (default: session)
"rotationInterval" : 30, // Token lifetime in minutes (0 = never expire)
"timeoutSkew" : 120, // Seconds before expiry to force-rotate
"reapFrequency" : 1, // Minutes between expired-token cleanup
"autoVerify" : false, // Auto-verify tokens on state-changing requests
"headerName" : "x-csrf-token",
"verifyMethods" : ["POST", "PUT", "PATCH", "DELETE"]
}
}
}
}
Auto-Verification
When autoVerify: true, every POST/PUT/PATCH/DELETE request is automatically checked for a valid CSRF token in the x-csrf-token header. No manual verification code needed.
Common Pitfalls
- ✅ Always include the CSRF token for any state-changing request (POST, PUT, DELETE, PATCH)
- ❌ GET requests should NEVER change state — CSRF protection only applies to non-GET methods
- ✅ Use scoped tokens (
keyparam) to differentiate between multiple forms on the same page - ❌ Don't skip
CSRFRotate()after login/logout — stale tokens from old sessions are a risk - ✅ For SPA/AJAX apps, retrieve the token server-side and embed it in the page on initial render