Nosql Injection in Echo Go with Firestore
Nosql Injection in Echo Go with Firestore — how this specific combination creates or exposes the vulnerability
NoSQL injection in a Go service built with Echo and using Cloud Firestore occurs when user-controlled input is concatenated into queries or map structures that Firestore interprets as operators or field paths. Firestore does not use SQL, but its query language and document lookups still allow attacker-controlled values to change query semantics, bypass intended filters, or read unintended documents.
In Echo, route parameters, query strings, and JSON payloads are often bound directly into handler logic. If a handler builds a Firestore query by inserting these values into map keys, field paths, or array filters without validation, an attacker can inject Firestore operators such as $eq, $in, $gt, or path-like keys that traverse nested documents. For example, an attacker supplying {"username": {"$ne": ""}} as JSON can turn a lookup intended to match a single user into a query that matches many users or enumerates documents.
Another common pattern is using user input as a document ID or map key. If an Echo handler does docRef := client.Collection("users").Doc(userSuppliedID) with userSuppliedID taken directly from a query parameter, an attacker can use encoded slashes or reserved characters to traverse collections or access documents outside the intended scope. Firestore treats these values as literal path elements, so injection here is path traversal via document IDs rather than SQL-like tautologies.
Additionally, Firestore’s array-contains and array-contains-any operators can be abused when user input is placed inside array filters. If an Echo handler constructs a query like collection.Where("tags", "array-contains", userTag) with userTag attacker-controlled, they may force the query to match documents based on injected array values. In more advanced scenarios, combining user input with map structures that include operator-like keys can cause Firestore to interpret attacker data as query modifiers, bypassing intended read restrictions.
These patterns map to the broader NoSQL injection class described in the OWASP API Top 10 and can lead to information disclosure, privilege escalation, or data tampering. Because Firestore queries are constructed dynamically in Go code, the risk is tightly coupled to how strictly inputs are validated and how safely the query-building logic is written. Echo endpoints that accept JSON and directly marshal it into Firestore queries or document references without normalization or allowlisting are especially prone to these issues.
Firestore-Specific Remediation in Echo Go — concrete code fixes
Remediation focuses on strict input validation, avoiding dynamic operator injection, and never allowing user input to directly form Firestore field paths or map keys. Use allowlists for known values, parse user input into controlled structs, and rely on Firestore’s built-in document ID mechanisms rather than concatenating paths from raw input.
Example: Safe document retrieval by ID
Instead of using raw query parameters as document IDs, validate and normalize the ID. Firestore document IDs should not contain slashes or special characters that enable path traversal.
// Safe document lookup in Echo handler
import (
"github.com/labstack/echo/v4"
"cloud.google.com/go/firestore"
)
func getUserHandler(c echo.Context) error {
userID := c.QueryParam("id")
// Basic allowlist: only alphanumeric and underscore, length constraints
if !isValidUserID(userID) {
return echo.NewHTTPError(http.StatusBadRequest, "invalid user id")
}
ctx := c.Request().Context()
docRef := client.Collection("users").Doc(userID)
var user User
if err := docRef.Get(ctx, &user); err != nil {
if status.Code(err) == codes.NotFound {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
return echo.NewHTTPError(http.StatusInternalServerError, "failed to fetch user")
}
return c.JSON(http.StatusOK, user)
}
func isValidUserID(id string) bool {
// Allow letters, numbers, underscore; limit length
matched, _ := regexp.MatchString(`^[A-Za-z0-9_]{1,100}$`, id)
return matched
}
Example: Query with allowlisted field and value
Do not construct query maps from raw JSON. Instead, unmarshal into a controlled struct and build queries with explicit field names and operators.
// Safe query construction in Echo handler
type QueryParams struct {
Status string `json:"status"`
Limit int `json:"limit"`
}
func listItemsHandler(c echo.Context) error {
p := new(QueryParams)
if err := c.Bind(p); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
}
// Allowlist status values
allowedStatus := map[string]bool{"active": true, "inactive": true, "pending": true}
if p.Status != "" && !allowedStatus[p.Status] {
return echo.NewHTTPError(http.StatusBadRequest, "invalid status")
}
ctx := c.Request().Context()
query := client.Collection("items").Limit(int64(p.Limit))
if p.Status != "" {
query = query.Where("status", "==", p.Status)
}
iter := query.Documents(ctx)
defer iter.Stop()
var results []Item
for {
item, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to query")
}
var it Item
if err := item.DataTo(&it); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to decode")
}
results = append(results, it)
}
return c.JSON(http.StatusOK, results)
}
Avoiding operator injection in map-based queries
Never marshal user JSON directly into a Firestore query map. If you must accept dynamic filters, parse and transform them server-side into safe field/operator pairs.
// Unsafe: directly using user input in a map
// BAD: rawMap := map[string]interface{}{"username": userInput}
// This can allow keys like "$ne" or nested paths.
// Safe alternative: explicit field and allowlisted operators
func filterUsersHandler(c echo.Context) error {
var req struct {
Field string `json:"field"`
Operator string `json:"operator"`
Value string `json:"value"`
}
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid body")
}
// Allowlist fields and operators
allowedFields := map[string]bool{"email": true, "status": true}
allowedOps := map[string]bool{"==": true, "!=": true, ">': true, "<": true}
if !allowedFields[req.Field] || !allowedOps[req.Operator] {
return echo.NewHTTPError(http.StatusBadRequest, "invalid filter")
}
ctx := c.Request().Context()
query := client.Collection("users").Where(req.Field, req.Operator, req.Value)
iter := query.Documents(ctx)
defer iter.Stop()
var users []User
for {
doc, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "query failed")
}
var u User
if err := doc.DataTo(&u); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "decode failed")
}
users = append(users, u)
}
return c.JSON(http.StatusOK, users)
}