CWE-943: Improper Neutralization of Special Elements in Data Query Logic - Go
Overview
NoSQL injection vulnerabilities in Go applications occur when user-controlled data is incorporated into NoSQL database queries without proper validation or sanitization, allowing attackers to manipulate query logic, access unauthorized data, bypass authentication, or cause data corruption. NoSQL databases (MongoDB, Redis, CouchDB, Cassandra) use different query languages than SQL - often JSON-like structures, key-value operations, or custom DSLs - but are equally vulnerable to injection attacks when user input isn't properly handled.
MongoDB is particularly vulnerable due to its BSON query format and JavaScript evaluation capabilities (where still enabled). Attackers inject MongoDB query operators ($where, $ne, $gt, $regex) to modify query logic. For example, authentication bypass by changing {"username": "admin", "password": "user_input"} to {"username": "admin", "password": {"$ne": null}} which always matches. Redis commands can be injected through Lua script parameters. CouchDB's map-reduce functions accept JavaScript that can be exploited. Even document-oriented databases with complex query DSLs are vulnerable to operator injection.
Common vulnerable patterns include building queries using string concatenation with user input, accepting raw JSON from clients and passing it to database drivers, using database map[] structures populated directly from request parameters, and enabling dangerous database features like MongoDB's $where operator (executing JavaScript) or server-side JavaScript evaluation. The impact ranges from unauthorized data access and privilege escalation to complete database compromise.
Primary Defence: Use driver parameterization and structured queries. Validate and allowlist user input. Never allow raw operator injection ($where, $ne, $regex, etc.). Use MongoDB's find() with struct binding or explicit BSON builders. Disable dangerous features like JavaScript execution. Apply principle of least privilege to database accounts.
Common Vulnerable Patterns
MongoDB Operator Injection via map[string]interface{}
// VULNERABLE - MongoDB query with map from user input
package main
import (
"context"
"fmt"
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
var collection *mongo.Collection
func findUserHandler(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
// DANGEROUS: Building query with user-controlled values
filter := bson.M{
"username": username,
"password": password,
}
var user map[string]interface{}
err := collection.FindOne(context.Background(), filter).Decode(&user)
if err == nil {
fmt.Fprintf(w, "Authentication successful: %v", user)
} else {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
}
}
// ATTACK:
// /findUser?username=admin&password[$ne]=null
// Becomes: {"username": "admin", "password": {"$ne": null}}
// MongoDB interprets $ne as operator, matches any password != null
// Authentication bypassed without knowing the password
Why this is vulnerable: HTTP query parameters can include special characters and nested structures. Parsing password[$ne]=null creates a map structure {"password": {"$ne": null}}. MongoDB interprets $ne (not equal) as an operator, not a literal string. The query matches any user where password is not null (all users), bypassing authentication. Similar attacks use $gt, $lt, $regex, $where, and other MongoDB operators. The application treats user input as trusted query structure rather than data.
JSON Deserialization to Query Filter
// VULNERABLE - Accepting raw JSON query from client
import (
"encoding/json"
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
func queryDataHandler(w http.ResponseWriter, r *http.Request) {
// DANGEROUS: User provides entire query structure
var filter bson.M
if err := json.NewDecoder(r.Body).Decode(&filter); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Execute user-controlled query
cursor, err := collection.Find(context.Background(), filter)
if err != nil {
http.Error(w, "Query failed", http.StatusInternalServerError)
return
}
defer cursor.Close(context.Background())
var results []bson.M
cursor.All(context.Background(), &results)
json.NewEncoder(w).Encode(results)
}
// ATTACK:
// POST /query
// {"role": {"$ne": "admin"}} -> Returns all non-admin users
// {"role": "admin"} -> Returns all admin users
// {"$where": "this.balance > 1000000"} -> Arbitrary JavaScript execution (if enabled)
// {"password": {"$regex": "^a"}} -> Password enumeration via regex
Why this is vulnerable: Accepting arbitrary JSON from clients gives attackers complete control over query structure. They can inject any MongoDB operator to access data beyond their authorization: $ne to match everything except a value, $gt/$lt for range queries, $regex for pattern matching (password enumeration), $where for JavaScript injection (if not disabled), or $elemMatch to query array fields. Attackers bypass intended access controls by modifying query logic to return unauthorized data.
String Concatenation in Redis
// VULNERABLE - Redis command injection via string concatenation
import (
"context"
"fmt"
"net/http"
"github.com/go-redis/redis/v8"
)
var redisClient *redis.Client
func getUserData(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
// DANGEROUS: String concatenation in Redis key
key := "user:" + userID
val, err := redisClient.Get(context.Background(), key).Result()
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
fmt.Fprintf(w, "User data: %s", val)
}
// ATTACK:
// /getUserData?user_id=123%0ADEL%20important_key
// Attempts command injection: "user:123\nDEL important_key"
// While go-redis client sanitizes newlines, other patterns may be vulnerable
Why this is vulnerable: While the go-redis client sanitizes newlines preventing direct command injection, other Redis features remain vulnerable. User-controlled keys can access unintended data: user_id=../admin might access admin keys if key structure isn't validated. Lua script injection is possible if user input is passed to EVAL commands. Pattern matching with KEYS command can cause performance issues (scanning all keys). Proper input validation and allowlisting prevent these attacks.
Cassandra CQL Injection
// VULNERABLE - CQL query with string concatenation
import (
"fmt"
"net/http"
"github.com/gocql/gocql"
)
var session *gocql.Session
func getUserByEmail(w http.ResponseWriter, r *http.Request) {
email := r.URL.Query().Get("email")
// DANGEROUS: String interpolation in CQL
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
iter := session.Query(query).Iter()
defer iter.Close()
var user map[string]interface{}
if iter.MapScan(user) {
fmt.Fprintf(w, "User: %v", user)
}
}
// ATTACK:
// /getUserByEmail?email=' OR email != ''
// Query becomes: SELECT * FROM users WHERE email = '' OR email != ''
// Returns all users (bypasses email filter)
Why this is vulnerable: CQL (Cassandra Query Language) is SQL-like and vulnerable to injection via string concatenation. Attackers inject single quotes to break out of string literals and add OR conditions (' OR email != ''), UNION queries (in some contexts), or other CQL operators. Even though Cassandra doesn't support complex joins/subqueries like SQL, injection still allows unauthorized data access, bypassing intended filtering. Always use parameterized queries.
JavaScript Injection via $where
// VULNERABLE - MongoDB $where operator (JavaScript injection)
import (
"context"
"fmt"
"net/http"
"go.mongodb.org/mongo-driver/bson"
)
func searchProducts(w http.ResponseWriter, r *http.Request) {
searchTerm := r.URL.Query().Get("search")
// DANGEROUS: Using $where with user input
filter := bson.M{
"$where": fmt.Sprintf("this.name.includes('%s')", searchTerm),
}
cursor, _ := collection.Find(context.Background(), filter)
// ... return results
}
// ATTACK:
// /searchProducts?search=') || (this.role == 'admin
// Query: this.name.includes('') || (this.role == 'admin')
// Returns all admin records
//
// Worse:
// /searchProducts?search='); while(true){}; //
// Causes denial of service with infinite loop
Why this is vulnerable: MongoDB's $where operator executes JavaScript on the server side (deprecated and disabled by default in recent versions, but still supported). User input in JavaScript strings can break out with quotes and inject arbitrary code: closing the string, adding OR conditions to bypass filters, or executing malicious JavaScript (infinite loops for DoS, accessing global objects, calling functions). Never use $where with user input. Prefer standard MongoDB query operators which don't execute code.
Secure Patterns
MongoDB with Typed Structs and Parameterization
// SECURE - Using typed structs for MongoDB queries
package main
import (
"context"
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type User struct {
Username string `bson:"username"`
Password string `bson:"password_hash"`
Role string `bson:"role"`
}
func secureLogin(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
// SECURE: Validate input format
if !isValidUsername(username) {
http.Error(w, "Invalid username format", http.StatusBadRequest)
return
}
// SECURE: Use typed struct for filter (prevents operator injection)
filter := User{
Username: username,
}
var user User
err := collection.FindOne(context.Background(), filter).Decode(&user)
if err != nil {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
// SECURE: Verify password hash
if !verifyPasswordHash(user.Password, password) {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
// Grant access
createSession(w, user)
}
func isValidUsername(username string) bool {
// SECURE: Validate username format
if len(username) < 3 || len(username) > 32 {
return false
}
// Only alphanumeric and underscore
for _, r := range username {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '_') {
return false
}
}
return true
}
func verifyPasswordHash(hash, password string) bool {
// Implementation: bcrypt.CompareHashAndPassword
return true
}
func createSession(w http.ResponseWriter, user User) {}
Why this works: Using a typed struct (User) prevents operator injection - the MongoDB driver serializes the struct directly to BSON without interpreting special characters or operators. Even if username contains $ne or other operators, they're treated as literal string values, not query operators. Input validation (isValidUsername) restricts characters to prevent unexpected patterns. Password verification uses hash comparison, never querying by password directly. This is the safe-by-default approach for MongoDB queries.
Explicit BSON Building with Validation
// SECURE - Explicit BSON construction with allowlisting
import (
"context"
"fmt"
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func secureSearch(w http.ResponseWriter, r *http.Request) {
category := r.URL.Query().Get("category")
minPrice := r.URL.Query().Get("min_price")
// SECURE: Validate and allowlist category
validCategories := map[string]bool{
"electronics": true,
"clothing": true,
"books": true,
}
if !validCategories[category] {
http.Error(w, "Invalid category", http.StatusBadRequest)
return
}
// SECURE: Parse and validate numeric input
minPriceInt, err := parseInt(minPrice)
if err != nil || minPriceInt < 0 {
http.Error(w, "Invalid price", http.StatusBadRequest)
return
}
// SECURE: Explicitly build BSON with validated values
filter := bson.D{
{Key: "category", Value: category},
{Key: "price", Value: bson.D{{Key: "$gte", Value: minPriceInt}}},
}
cursor, err := collection.Find(context.Background(), filter)
if err != nil {
http.Error(w, "Search failed", http.StatusInternalServerError)
return
}
defer cursor.Close(context.Background())
// Process results...
}
func parseInt(s string) (int, error) {
var result int
_, err := fmt.Sscanf(s, "%d", &result)
return result, err
}
Why this works: Allowlisting permitted categories prevents injection of unexpected values. Numeric parsing ensures min_price is truly a number, not a string containing operators. bson.D (ordered document) explicitly constructs the query structure - operators like $gte are added programmatically, not from user input. Users cannot inject arbitrary operators because only validated, allowlisted values are incorporated into the query. The query structure is controlled by the application, not the user.
Parameterized Queries for CQL (Cassandra)
// SECURE - CQL with parameterized queries
import (
"net/http"
"github.com/gocql/gocql"
)
func secureGetUserByEmail(w http.ResponseWriter, r *http.Request) {
email := r.URL.Query().Get("email")
// SECURE: Validate email format
if !isValidEmail(email) {
http.Error(w, "Invalid email format", http.StatusBadRequest)
return
}
// SECURE: Parameterized query (? placeholder)
query := "SELECT user_id, username, role FROM users WHERE email = ?"
var userID gocql.UUID
var username, role string
err := session.Query(query, email).Scan(&userID, &username, &role)
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
fmt.Fprintf(w, "User: %s, Role: %s", username, role)
}
func isValidEmail(email string) bool {
// Simplified email validation
return len(email) > 3 && contains(email, "@") && contains(email, ".")
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && indexOf(s, substr) >= 0
}
func indexOf(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
Why this works: The ? placeholder in CQL queries represents a parameter. The gocql library handles escaping and proper data typing automatically. Users cannot break out of string literals or inject CQL syntax because parameters are safely interpolated by the driver. Even if email contains single quotes or CQL keywords, they're treated as literal data. Input validation provides defense-in-depth by rejecting malformed emails early. Parameterized queries are the standard approach for injection prevention across all database types.
Redis with Structured Commands
// SECURE - Redis with client API (not raw commands)
import (
"context"
"net/http"
"regexp"
"github.com/go-redis/redis/v8"
)
var userIDPattern = regexp.MustCompile(`^[0-9]+$`)
func secureGetUserData(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
// SECURE: Validate user ID is numeric only
if !userIDPattern.MatchString(userID) {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
// SECURE: Use client's Get method (structured API)
key := "user:" + userID
val, err := redisClient.Get(context.Background(), key).Result()
if err == redis.Nil {
http.Error(w, "User not found", http.StatusNotFound)
return
} else if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(val))
}
func secureIncrementCounter(userID string) error {
// SECURE: Validated input + structured command
if !userIDPattern.MatchString(userID) {
return fmt.Errorf("invalid user ID")
}
key := "counter:" + userID
return redisClient.Incr(context.Background(), key).Err()
}
Why this works: The go-redis client provides type-safe methods (Get, Set, Incr, etc.) that construct Redis commands internally. Users cannot inject raw Redis commands because the client API doesn't accept raw command strings. Input validation (regex pattern matching) ensures userID contains only digits, preventing path traversal in key names or special Redis characters. Key structure is controlled by the application (user: prefix), preventing access to unintended keys. For Lua scripts, use parameterized EVAL with arguments, not string concatenation.
Defense in Depth with Access Controls
// SECURE - MongoDB with role-based access control
import (
"context"
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Document struct {
ID string `bson:"_id"`
OwnerID string `bson:"owner_id"`
Data string `bson:"data"`
}
func secureGetDocument(w http.ResponseWriter, r *http.Request) {
docID := r.URL.Query().Get("doc_id")
// Get authenticated user from session
userID := getUserIDFromSession(r)
if userID == "" {
http.Error(w, "Not authenticated", http.StatusUnauthorized)
return
}
// SECURE: Filter by both document ID AND owner
filter := bson.D{
{Key: "_id", Value: docID},
{Key: "owner_id", Value: userID},
}
// SECURE: Project only necessary fields
projection := bson.D{
{Key: "data", Value: 1},
{Key: "_id", Value: 1},
}
opts := options.FindOne().SetProjection(projection)
var doc Document
err := collection.FindOne(context.Background(), filter, opts).Decode(&doc)
if err != nil {
http.Error(w, "Document not found or access denied", http.StatusNotFound)
return
}
fmt.Fprintf(w, "Document: %s", doc.Data)
}
func getUserIDFromSession(r *http.Request) string {
// Implementation: extract from session cookie
return "user123"
}
Why this works: Defense-in-depth combines multiple security layers. The query explicitly filters by owner_id, enforcing access control at the database level even if application logic is bypassed. Projection limits returned fields to only what's needed, preventing information leakage. Session authentication ensures users are identified before any database query. Even if injection were somehow possible, the owner_id filter restricts results to the authenticated user's data. Combining input validation, parameterization, and access control provides maximum security.
Framework-Specific Guidance
MongoDB Official Driver Best Practices
// SECURE - MongoDB best practices with official driver
import (
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func secureMongoDBQuery(userInput string, userRole string) ([]bson.M, error) {
// SECURE: Validate input
if !isValidInput(userInput) {
return nil, fmt.Errorf("invalid input")
}
// SECURE: Use bson.D for deterministic field order
filter := bson.D{
{Key: "category", Value: userInput},
{Key: "published", Value: true},
}
// Role-based field projection
projection := bson.D{
{Key: "title", Value: 1},
{Key: "description", Value: 1},
}
if userRole == "admin" {
projection = append(projection, bson.E{Key: "internal_notes", Value: 1})
}
opts := options.Find().
SetProjection(projection).
SetLimit(100).
SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := collection.Find(context.Background(), filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(context.Background())
var results []bson.M
if err := cursor.All(context.Background(), &results); err != nil {
return nil, err
}
return results, nil
}
func isValidInput(input string) bool {
// Validation logic
return len(input) > 0 && len(input) < 100
}
Why this works: Using bson.D ensures deterministic field order and type-safe query construction. Operators are explicitly added by the application, not user input. Options like SetLimit prevent resource exhaustion. Role-based projection controls field visibility. The MongoDB driver handles all escaping and type conversion, preventing injection. This pattern separates validation (application logic) from database operations (driver responsibility).