Cross Site Request Forgery in Buffalo with Firestore
Cross Site Request Forgery in Buffalo with Firestore — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) in a Buffalo application using Firestore as the backend can occur when authenticated Firestore operations are invoked from endpoints without anti-CSRF protections. Buffalo provides server-side session management and can set secure cookies, but if application routes directly perform Firestore writes (e.g., document updates or deletions) based solely on user session state and do not verify the request origin, an attacker can trick a logged-in user into submitting a forged request.
Consider a Buffalo handler that updates a user profile document in Firestore using the user ID from the session:
// handlers/user.go
func UpdateProfile(c buffalo.Context) error {
userID := c.Session().Get("user_id")
if userID == nil {
return c.Render(401, rms.Error("unauthorized"))
}
ref := client.Collection("users").Doc(userID.(string))
// Bind JSON directly from request body into Firestore document
var data map[string]interface{}
if err := c.Bind(&data); err != nil {
return c.Render(400, rms.Error("invalid request"))
}
_, err := ref.Update(c, data)
if err != nil {
return c.Render(500, rms.Error("update failed"))
}
return c.Render(200, rms.Map{"status": "ok"})
}
If this endpoint accepts POST requests and does not validate the Origin header or include CSRF tokens, an attacker can craft a malicious site that triggers this handler on behalf of the victim. Because the handler uses the session-derived userID, Firestore will apply updates in the victim’s context. The vulnerability is not in Firestore itself — Firestore enforces security rules — but in the application layer where Firestore operations are invoked without CSRF-specific checks. Attack vectors include forms or image tags on attacker-controlled pages that submit to the Buffalo endpoint, exploiting the browser’s automatic inclusion of cookies (including session cookies) to perform actions the user did not intend.
Additionally, if the Buffalo app exposes an unauthenticated Firestore endpoint (for example, a public read-only API route that also accepts writes), and does not enforce strict CORS or origin validation, the risk increases. Firestore security rules may permit writes if they rely only on request authentication that the attacker can bypass via CSRF, especially if rules are misconfigured to trust request origins implicitly.
Firestore-Specific Remediation in Buffalo — concrete code fixes
Remediation focuses on ensuring every state-changing Firestore operation in Buffalo is protected with CSRF defenses and strict origin validation. Below are concrete code examples demonstrating safe patterns.
1. Use CSRF tokens for state-changing requests
Buffalo has built-in CSRF support via the csrf middleware. Enable it globally and require tokens for writes:
// app.go
import (
"github.com/gobuffalo/buffalo"
"github.com/gobuffalo/buffalo/middleware"
)
func App() *buffalo.App {
app := buffalo.New(buffalo.Options{})
// Apply CSRF middleware to all routes; excludes GET, HEAD, OPTIONS by default
app.Use(middleware.CSRF)
// Your routes...
return app
}
In your handler, ensure the token is validated (middleware does this automatically) and include the token in forms:
<!-- templates/user_edit.html -->
<form method="POST" action="/users/{{.UserID}}/profile">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<input name="display_name" value="{{ .Profile.DisplayName }}">
<button type="submit">Save</button>
</form>
2. Validate Origin and Referer headers for Firestore write endpoints
Add a custom middleware to verify the request origin for sensitive routes:
// middleware/csrf_origin.go
package middleware
import (
"net/http"
"strings"
)
func ValidateOrigin(allowedOrigin string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if origin != allowedOrigin {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
Apply this selectively in routes where Firestore writes occur:
// app.go
app.POST("/users/:userID/profile", ValidateOrigin("https://yourapp.com"), UserHandlers{}.UpdateProfile)
3. Firestore security rules aligned with Buffalo session handling
Ensure Firestore rules validate not only authentication but also that the request’s user identity matches the document being modified. Use custom claims or tokens passed from Buffalo to Firestore (e.g., via Firebase Admin SDK on the server) to enforce ownership:
// Firestore rule example
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userID} {
allow write: if request.auth != null && request.auth.uid == userID;
allow read: if request.auth != null && request.auth.uid == userID;
}
}
}
4. Avoid binding arbitrary data directly to Firestore updates; use explicit structs
Replace blind map binding with a validated struct to prevent mass assignment:
// models/user.go
package models
type ProfileUpdate struct {
DisplayName string `json:"display_name"`
Email string `json:"email"`
}
// handlers/user.go
func (h UserHandlers) UpdateProfile(c buffalo.Context) error {
userID := c.Session().Get("user_id")
if userID == nil {
return c.Render(401, rms.Error("unauthorized"))
}
var req models.ProfileUpdate
if err := c.Bind(&req); err != nil {
return c.Render(400, rms.Error("invalid request"))
}
ref := client.Collection("users").Doc(userID.(string))
_, err := ref.Update(c, map[string]interface{}{
"display_name": req.DisplayName,
"email": req.Email,
})
if err != nil {
return c.Render(500, rms.Error("update failed"))
}
return c.Render(200, rms.Map{"status": "ok"})
}
By combining Buffalo’s CSRF middleware, strict origin checks, and disciplined Firestore rule design, the CSRF attack surface is significantly reduced while maintaining secure, authenticated writes to Firestore.