Broken Access Control in Fiber with Firestore
Broken Access Control in Fiber with Firestore — how this specific combination creates or exposes the vulnerability
Broken Access Control (BOLA/IDOR) in a Fiber application that uses Firestore as a backend often arises from trusting client-supplied identifiers without verifying authorization. Firestore security rules are not a substitute for application-level authorization; they are a last-line defense. When access checks are implemented only in rules and not enforced in the Fiber handler, an authenticated user can modify URL parameters (e.g., userID, documentID) to access or mutate other users’ data.
Consider a route like /users/:userID/profile. If the handler retrieves the profile using the parameter directly and returns the document without confirming that the authenticated user matches userID, the endpoint becomes an IDOR vector. Firestore rules might allow read access if request.auth != null, but they won’t prevent a user from reading another document if the document path is parameterized by an ID the user can guess or iterate. This is a BOLA (Broken Level of Access) pattern: a user accesses another’s resource simply by changing an identifier.
Additionally, Firestore’s rule syntax can inadvertently grant broader access than intended. For example, a rule like allow read: if request.auth != null; on a collection grants read to all authenticated users, which is too permissive when each user should only access their own documents. Without pairing this with application checks, the API surface expands the attack window. Attackers can enumerate valid IDs or leverage predictable UUIDs to scrape data, leading to sensitive data exposure.
Real-world exploitation steps often include intercepting a valid request, altering the resource identifier, and observing whether the server enforces ownership. For instance, changing userID=abc123 to userID=def456 should return 403, not the target user’s profile. In practice, many Fiber services skip this authorization check to keep handlers simple, mistakenly assuming Firestore rules will block unauthorized reads. This assumption is risky because rules can be misconfigured, and runtime behavior may differ from development environments.
Another subtle vector involves Firestore subcollections and composite paths. If a route traverses a path like users/{userID}/orders/{orderID}, the handler must validate that the authenticated user owns both the user and the order. If only the top-level document is checked, or if the rule for subcollections is open, an attacker can pivot across relationships. This is especially relevant when using Firestore’s deep paths for convenience without re-evaluating authorization at each level.
The combination of Fiber’s lightweight routing and Firestore’s flexible data model amplifies the need for explicit, per-request authorization. Relying solely on security rules or on client-provided IDs without server-side checks turns the API into a direct channel for horizontal privilege escalation. Using middleBrick’s BOLA/IDOR checks can surface these gaps by testing unauthenticated and authenticated traversal paths to confirm ownership is enforced at the application layer, not just in rules.
Firestore-Specific Remediation in Fiber — concrete code fixes
Remediation centers on enforcing ownership checks in the Fiber handler before any Firestore operation. Never trust route parameters; always map them to the authenticated user’s identity and validate server-side. Below are concrete, idiomatic examples using the official Firestore Go SDK and the Fiber framework.
1. Validate userID against authenticated UID
For a route that accesses user-specific data, extract the authenticated UID from the context and compare it to the parameter before constructing the Firestore reference.
// handler.go
package handlers
import (
"github.com/gofiber/fiber/v2"
"cloud.google.com/go/firestore"
"context"
)
func GetUserProfile(db *firestore.Client) fiber.Handler {
return func(c *fiber.Ctx) error {
userID := c.Params("userID")
// Assume GetCurrentUID returns the UID from auth token/session
currentUID := GetCurrentUID(c)
if userID != currentUID {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "access denied"})
}
ctx := context.Background()
doc, err := db.Collection("users").Doc(userID).Get(ctx)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to fetch profile"})
}
if !doc.Exists() {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "not found"})
}
return c.JSON(doc.Data())
}
}
2. Enforce ownership in Firestore rules as a secondary defense
While application checks are primary, complement them with strict Firestore rules that reference request.auth.uid. Avoid broad reads on collections; scope reads to the user’s document path.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Subcollection example
match /users/{userId}/orders/{orderId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
3. Use Firestore queries that bind to the authenticated UID
When listing or searching within a user’s context, structure queries to include the UID as a filter, avoiding client-supplied filters that can be manipulated.
// listUserOrders ensures the query is scoped to the current user
func ListUserOrders(db *firestore.Client) fiber.Handler {
return func(c *fiber.Ctx) error {
currentUID := GetCurrentUID(c)
ctx := context.Background()
iter := db.Collection("users").Doc(currentUID).Collection("orders").Where("status", "==", "open").Documents(ctx)
defer iter.Stop()
var orders []map[string]interface{}
for {
doc, err := iter.Next()
if err != nil {
break
}
orders = append(orders, doc.Data())
}
return c.JSON(orders)
}
}
4. Avoid Firestore rule patterns that enable broad access
Do not use rules that allow read/write on a collection based only on authentication state without user scoping. Instead, explicitly bind to document paths that include the UID.
// Avoid: too permissive
allow read: if request.auth != null;
// Prefer: scoped to user document
allow read: if request.auth != null && request.auth.uid == request.resource.id;
5. Validate and sanitize inputs before Firestore operations
Even with ownership checks, validate that the parameter conforms to expected formats to prevent injection or path traversal attempts. Use Firestore’s built-in key validation and avoid concatenating raw strings into paths.
// Example validation snippet
if userID == "" || !isValidUserID(userID) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid user identifier"})
}
By combining strict handler-level authorization with scoped Firestore rules and parameterized queries, you reduce the risk of BOLA/IDOR in a Fiber + Firestore stack. middleBrick’s scans can verify that your endpoints enforce ownership consistently across authenticated paths.