Skip to content

CSRF Protection

Cross-Site Request Forgery (CSRF) is an attack that tricks users into performing unwanted actions on a web application where they’re authenticated. Fluxbase provides built-in CSRF protection to prevent these attacks.

CSRF attacks exploit the trust a web application has in a user’s browser. If a user is logged into your application, an attacker can trick their browser into making requests to your application without their knowledge.

  1. User logs into https://yourapp.com
  2. User visits malicious site https://evil.com
  3. Malicious site contains:
    <img src="https://yourapp.com/api/v1/tables/users?delete=all" />
  4. Browser automatically includes authentication cookies
  5. Unintended action is performed

Fluxbase implements the Double-Submit Cookie pattern:

  1. Server generates a random CSRF token
  2. Token is stored in:
    • HTTP-only cookie (not accessible to JavaScript)
    • Response header/body (for client to read)
  3. Client includes token in subsequent requests
  4. Server validates both tokens match
Client Server
| |
|-- GET /api/v1/users --------->|
| | Generate CSRF token
|<------- Set-Cookie ----------| Set csrf_token cookie
| |
|-- POST /api/v1/users -------->|
| X-CSRF-Token: abc123 | Validate:
| Cookie: csrf_token=abc123 | - Cookie matches header
| | - Token exists in storage
|<------- 200 OK ---------------|

CSRF protection is enabled by default for state-changing methods (POST, PUT, PATCH, DELETE).

Configuration via fluxbase.yaml:

security:
csrf:
enabled: true
token_length: 32
token_lookup: "header:X-CSRF-Token"
cookie_name: "csrf_token"
cookie_secure: true # Set to true in production
cookie_http_only: true
cookie_same_site: "Strict"
expiration: "24h"

Configuration via Environment Variables:

Terminal window
FLUXBASE_SECURITY_CSRF_ENABLED=true
FLUXBASE_SECURITY_CSRF_TOKEN_LENGTH=32
FLUXBASE_SECURITY_CSRF_COOKIE_NAME=csrf_token
FLUXBASE_SECURITY_CSRF_COOKIE_SECURE=true
FLUXBASE_SECURITY_CSRF_COOKIE_SAME_SITE=Strict
OptionDefaultDescription
enabledtrueEnable/disable CSRF protection
token_length32Length of CSRF token in bytes
token_lookupheader:X-CSRF-TokenWhere to find the token in requests
cookie_namecsrf_tokenName of the CSRF cookie
cookie_securefalseMark cookie as HTTPS-only
cookie_http_onlytruePrevent JavaScript access to cookie
cookie_same_siteStrictSameSite attribute (Strict, Lax, None)
expiration24hHow long tokens are valid
  • Strict: Cookie only sent for same-site requests (most secure)
  • Lax: Cookie sent for same-site requests and top-level navigation
  • None: Cookie sent for all requests (requires Secure flag)
# Recommended for most applications
cookie_same_site: "Strict"
# For cross-site authentication flows
cookie_same_site: "Lax"
# For third-party integrations (requires HTTPS)
cookie_same_site: "None"
cookie_secure: true

// 1. Get CSRF token from cookie
function getCsrfToken(): string | null {
const match = document.cookie.match(/csrf_token=([^;]+)/);
return match ? match[1] : null;
}
// 2. Include token in requests
async function makeRequest(url: string, data: any) {
const csrfToken = getCsrfToken();
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken || "", // Include CSRF token
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(data),
credentials: "include", // Include cookies
});
if (!response.ok) {
throw new Error("Request failed");
}
return response.json();
}
// Example usage
makeRequest("/api/v1/tables/users", {
name: "John Doe",
email: "john@example.com",
});

The Fluxbase SDK handles CSRF tokens automatically:

import { createClient } from "@fluxbase/sdk";
const client = createClient("http://localhost:8080", "your-anon-key");
// CSRF token is automatically included
await client
.from("users")
.insert({
name: "John Doe",
email: "john@example.com",
})
.execute();
import { useState, useEffect } from "react";
function getCsrfToken(): string | null {
const match = document.cookie.match(/csrf_token=([^;]+)/);
return match ? match[1] : null;
}
function UserForm() {
const [csrfToken, setCsrfToken] = useState<string | null>(null);
useEffect(() => {
// Get CSRF token on mount
setCsrfToken(getCsrfToken());
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const response = await fetch("/api/v1/tables/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken || "",
},
body: JSON.stringify({
name: "John Doe",
email: "john@example.com",
}),
credentials: "include",
});
if (!response.ok) {
console.error("Request failed");
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit">Submit</button>
</form>
);
}
<template>
<form @submit.prevent="handleSubmit">
<!-- Form fields -->
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
methods: {
getCsrfToken() {
const match = document.cookie.match(/csrf_token=([^;]+)/);
return match ? match[1] : null;
},
async handleSubmit() {
const csrfToken = this.getCsrfToken();
const response = await fetch("/api/v1/tables/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken || "",
},
body: JSON.stringify({
name: "John Doe",
email: "john@example.com",
}),
credentials: "include",
});
if (!response.ok) {
console.error("Request failed");
}
},
},
};
</script>
import axios from "axios";
// Create Axios instance
const api = axios.create({
baseURL: "http://localhost:8080",
withCredentials: true, // Include cookies
});
// Add CSRF token to all requests
api.interceptors.request.use((config) => {
const match = document.cookie.match(/csrf_token=([^;]+)/);
const csrfToken = match ? match[1] : null;
if (csrfToken && config.headers) {
config.headers["X-CSRF-Token"] = csrfToken;
}
return config;
});
// Usage
api.post("/api/v1/tables/users", {
name: "John Doe",
email: "john@example.com",
});

If you’re building custom endpoints, use the CSRF middleware:

package main
import (
"github.com/gofiber/fiber/v2"
"github.com/fluxbase-eu/fluxbase/internal/middleware"
)
func main() {
app := fiber.New()
// Apply CSRF middleware
app.Use(middleware.CSRF(middleware.CSRFConfig{
TokenLength: 32,
TokenLookup: "header:X-CSRF-Token",
CookieName: "csrf_token",
CookieSecure: true,
CookieHTTPOnly: true,
CookieSameSite: "Strict",
}))
// Your routes
app.Post("/api/users", createUser)
app.Listen(":8080")
}

Some paths are automatically excluded from CSRF protection:

  • Safe methods: GET, HEAD, OPTIONS
  • WebSocket endpoint: /realtime
  • Health checks: /health, /ready
  • Metrics: /metrics
  • API requests with API key authentication

1. Test without CSRF token:

Terminal window
# This should fail with 403 Forbidden
curl -X POST http://localhost:8080/api/v1/tables/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"name":"John Doe"}'

2. Test with valid CSRF token:

Terminal window
# First, get the CSRF token (from browser cookies or initial request)
CSRF_TOKEN="your-csrf-token-here"
# This should succeed
curl -X POST http://localhost:8080/api/v1/tables/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-CSRF-Token: $CSRF_TOKEN" \
-b "csrf_token=$CSRF_TOKEN" \
-d '{"name":"John Doe"}'
import { describe, it, expect } from "vitest";
import { createClient } from "@fluxbase/sdk";
describe("CSRF Protection", () => {
it("should reject requests without CSRF token", async () => {
const client = createClient("http://localhost:8080", "your-anon-key");
await client.auth.signIn({
email: "user@example.com",
password: "password",
});
try {
// Manually make request without CSRF token
await fetch("http://localhost:8080/api/v1/tables/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${client.getAuthToken()}`,
},
body: JSON.stringify({ name: "John" }),
});
// Should not reach here
expect(true).toBe(false);
} catch (error) {
expect(error.response.status).toBe(403);
}
});
it("should accept requests with valid CSRF token", async () => {
const client = createClient("http://localhost:8080", "your-anon-key");
await client.auth.signIn({
email: "user@example.com",
password: "password",
});
// SDK automatically handles CSRF
const { data, error } = await client
.from("users")
.insert({
name: "John Doe",
})
.execute();
expect(error).toBeNull();
expect(data).toBeDefined();
});
});

Cause: CSRF token missing or invalid

Solutions:

  1. Ensure cookie is being sent:

    fetch(url, {
    credentials: "include", // Include cookies
    });
  2. Check cookie domain matches:

    security:
    csrf:
    cookie_domain: ".yourdomain.com" # Allows subdomain.yourdomain.com
  3. Verify token is included in header:

    headers: {
    'X-CSRF-Token': csrfToken
    }

Cause: Token older than configured expiration

Solution: Request a new token by making a GET request:

// Make a simple GET request to get new CSRF token
await fetch("/api/v1/health");
// Token is now refreshed in cookie
const newToken = getCsrfToken();

Cause: Cross-origin requests with credentials

Solution: Configure CORS properly:

server:
cors:
allowed_origins:
- "https://yourdomain.com"
allow_credentials: true # Required for cookies
// Client must include credentials
fetch(url, {
credentials: "include", // Send cookies cross-origin
headers: {
"X-CSRF-Token": csrfToken,
},
});

Cause: Mobile apps don’t use cookies like browsers

Solution: Use API key authentication instead:

// Mobile app using API key (no CSRF needed)
const client = createClient("https://api.yourdomain.com", "your-anon-key");

For development or specific use cases, you can disable CSRF:

fluxbase.yaml
security:
csrf:
enabled: false

⚠️ Warning: Only disable CSRF if:

  • You’re in development
  • Using API key authentication exclusively
  • Using a different CSRF protection mechanism
  • Building a non-browser client (mobile app, CLI)

server:
tls:
enabled: true
cert_file: /path/to/cert.pem
key_file: /path/to/key.pem
security:
csrf:
cookie_secure: true # Requires HTTPS
security:
csrf:
cookie_same_site: "Strict" # Most secure
security:
csrf:
expiration: "24h" # Balance security and UX
// After password change, force token refresh
await client.auth.changePassword(oldPassword, newPassword);
// Make a GET request to get new CSRF token
await fetch("/api/v1/health");
// Log CSRF failures for security monitoring
app.Use(func(c *fiber.Ctx) error {
err := c.Next()
if err != nil && err.Error() == "CSRF token validation failed" {
log.Warn().
Str("ip", c.IP()).
Str("path", c.Path()).
Msg("CSRF validation failed")
}
return err
})

CSRF protection doesn’t prevent XSS attacks. Always:

  • Implement Content Security Policy
  • Sanitize user input
  • Use secure templating
  • Enable security headers

API key authentication bypasses CSRF protection:

  • API keys are not stored in cookies
  • Intended for server-to-server communication
  • Still need proper authentication and authorization

Never store CSRF tokens in:

  • ❌ LocalStorage (vulnerable to XSS)
  • ❌ SessionStorage (vulnerable to XSS)
  • ✅ HTTP-only cookies (safe from JavaScript)


Fluxbase provides robust CSRF protection out of the box:

  • Double-submit cookie pattern
  • Automatic token generation
  • HTTP-only cookies
  • SameSite attribute support
  • Configurable expiration
  • SDK handles tokens automatically

Enable CSRF protection in production and follow best practices to protect your users from CSRF attacks.