Skip to content

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

Additional Resources