HIGH nosql injectionecho gofirestore

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)
}

Frequently Asked Questions

Can NoSQL injection in Echo + Firestore expose or modify other users' data?
Yes, if user input is concatenated into document IDs, collection paths, or query maps without strict validation, attackers can read or reference documents outside the intended scope. Always validate and normalize IDs and use allowlists for field names and operators.
Does Firestore provide built-in protection against NoSQL injection?
Firestore enforces permissions and does not interpret operators in document IDs, but it will execute queries exactly as formed. It is the application’s responsibility to ensure that query structure and field paths are not derived from untrusted input. Validation and allowlisting on the server are required.