Skip to content

CWE-943: NoSQL Injection - C#

Overview

NoSQL Injection in C#/.NET applications occurs when untrusted input is used to construct NoSQL database queries (MongoDB, Redis, RavenDB, Azure Cosmos DB, etc.) without proper validation. Untrusted input can originate from HTTP requests, external APIs, databases, files, message queues, or any source outside the application's control. Attackers can exploit this to bypass authentication, extract sensitive data, modify database contents, or execute unauthorized operations.

Primary Defence: Use parameterized queries or the official MongoDB C# driver's LINQ-based query builder instead of string concatenation, validate and sanitize all user input with strict type checking before including in NoSQL queries, implement allowlists for query operators and field names, and use the driver's BsonDocument or strongly-typed filter expressions (Builders<T>.Filter) to prevent NoSQL injection attacks.

Common C# NoSQL Vulnerabilities:

  • MongoDB query operator injection using BsonDocument
  • Azure Cosmos DB SQL injection
  • Redis command injection via StackExchange.Redis
  • RavenDB query injection
  • LiteDB query injection

Popular C# NoSQL Libraries:

  • MongoDB.Driver: Official MongoDB driver for .NET
  • Microsoft.Azure.Cosmos: Azure Cosmos DB SDK
  • StackExchange.Redis: High-performance Redis client
  • RavenDB.Client: RavenDB .NET client
  • LiteDB: Embedded NoSQL database

Common Vulnerable Patterns

MongoDB Operator Injection

// VULNERABLE - Direct untrusted input in MongoDB query
using MongoDB.Driver;
using MongoDB.Bson;

public class UserService
{
    private IMongoCollection<BsonDocument> _users;

    public bool AuthenticateUser(string username, object password)
    {
        // VULNERABLE - Accepting object type allows operator injection
        var filter = Builders<BsonDocument>.Filter.And(
            Builders<BsonDocument>.Filter.Eq("username", username),
            Builders<BsonDocument>.Filter.Eq("password", password)
        );

        var user = _users.Find(filter).FirstOrDefault();
        return user != null;
    }
}

// Attack: password = new BsonDocument("$ne", BsonNull.Value)
// Query becomes: {username: "user", password: {$ne: null}}
// Authentication bypass!

Why this is vulnerable:

  • Accepts object type (can be BsonDocument with operators)
  • No type validation
  • MongoDB operators injectable
  • Authentication bypass

ASP.NET Core with Query Injection

// VULNERABLE - Accepting arbitrary filter from request
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Bson;
using System.Collections.Generic;

[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
    private IMongoCollection<BsonDocument> _products;

    [HttpPost("search")]
    public IActionResult SearchProducts([FromBody] Dictionary<string, object> query)
    {
        // VULNERABLE - Untrusted query dictionary
        var bsonDoc = new BsonDocument(query);
        var filter = new BsonDocumentFilterDefinition<BsonDocument>(bsonDoc);

        var products = _products.Find(filter).ToList();
        return Ok(products);
    }
}

// Attack POST body: {"price": {"$gt": 0}, "admin_only": {"$ne": true}}
// Bypasses access controls

Why this is vulnerable:

  • Arbitrary query dictionary from user
  • No field allowlist
  • Operator injection
  • Data exfiltration

MongoDB $where Injection

// VULNERABLE - JavaScript code injection via $where
using MongoDB.Driver;
using MongoDB.Bson;

public class UserRepository
{
    private IMongoCollection<BsonDocument> _users;

    public List<BsonDocument> FindUsersByAge(string minAge)
    {
        // VULNERABLE - String concatenation in $where
        var whereClause = $"this.age > {minAge}";
        var filter = new BsonDocument("$where", whereClause);

        return _users.Find(filter).ToList();
    }
}

// Attack: minAge = "0; return true; //"
// Executes arbitrary JavaScript on MongoDB server
// Returns all users

Why this is vulnerable:

  • $where executes JavaScript
  • String concatenation
  • Code injection
  • DoS attacks possible

Azure Cosmos DB SQL Injection

// VULNERABLE - Cosmos DB SQL query with string concatenation
using Microsoft.Azure.Cosmos;

public class CosmosService
{
    private Container _container;

    public async Task<List<dynamic>> SearchUsers(string role)
    {
        // VULNERABLE - String concatenation in SQL query
        string sql = $"SELECT * FROM c WHERE c.role = '{role}'";

        var query = _container.GetItemQueryIterator<dynamic>(sql);
        var results = new List<dynamic>();

        while (query.HasMoreResults)
        {
            var response = await query.ReadNextAsync();
            results.AddRange(response);
        }

        return results;
    }
}

// Attack: role = "user' OR 1=1--"
// SQL injection in Cosmos DB
// Returns all documents

Why this is vulnerable:

  • String concatenation in SQL
  • No parameterization
  • SQL injection
  • Data exfiltration

Redis Command Injection

// VULNERABLE - Redis with unsanitized keys
using StackExchange.Redis;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("cache")]
public class CacheController : ControllerBase
{
    private IDatabase _redis;

    [HttpGet("{key}")]
    public IActionResult GetCache(string key)
    {
        // VULNERABLE - Untrusted input in Redis key
        var value = _redis.StringGet(key);
        return Ok(value.ToString());
    }

    [HttpPost]
    public IActionResult SetCache([FromBody] CacheRequest request)
    {
        // VULNERABLE - No validation on key/value
        _redis.StringSet(request.Key, request.Value);
        return Ok();
    }
}

// Attack: key = "test\r\nFLUSHDB\r\n"
// Command injection (though StackExchange.Redis has some protection)

Why this is vulnerable:

  • No key validation
  • Potential CRLF injection
  • No input sanitization
  • Command injection attempts

MongoDB Aggregation Injection

// VULNERABLE - Aggregation pipeline with untrusted input
using MongoDB.Driver;
using MongoDB.Bson;
using System.Collections.Generic;

public class AnalyticsService
{
    private IMongoCollection<BsonDocument> _events;

    public List<BsonDocument> GetUserStats(string userId, string sortField)
    {
        // VULNERABLE - Untrusted input in aggregation pipeline
        var pipeline = new[]
        {
            new BsonDocument("$match", new BsonDocument("user_id", userId)),
            new BsonDocument("$sort", new BsonDocument(sortField, -1)),
            new BsonDocument("$limit", 10)
        };

        return _events.Aggregate<BsonDocument>(pipeline).ToList();
    }
}

// Attack: sortField = "$where"
// Can inject operators into pipeline

Why this is vulnerable:

  • No field validation
  • Operator injection
  • Pipeline manipulation
  • DoS attacks

MongoDB Regex Injection

// VULNERABLE - Regex injection in queries
using MongoDB.Driver;
using MongoDB.Bson;

public class SearchService
{
    private IMongoCollection<BsonDocument> _users;

    public List<BsonDocument> SearchUsers(string searchTerm)
    {
        // VULNERABLE - Untrusted input in regex without escaping
        var filter = Builders<BsonDocument>.Filter.Regex(
            "username", 
            new BsonRegularExpression(searchTerm, "i")
        );

        return _users.Find(filter).ToList();
    }
}

// Attack: searchTerm = ".*"
// Returns ALL users (DoS, data exfiltration)
// Attack: searchTerm = "(a+)+"
// ReDoS attack

Why this is vulnerable:

  • Regex patterns from untrusted sources
  • ReDoS attacks
  • No escaping
  • Information disclosure

RavenDB Query Injection

// VULNERABLE - RavenDB with raw RQL
using Raven.Client.Documents;
using Raven.Client.Documents.Session;

public class RavenService
{
    private IDocumentStore _store;

    public List<User> SearchUsers(string username)
    {
        using var session = _store.OpenSession();

        // VULNERABLE - String concatenation in RQL
        var query = $"from Users where username = '{username}'";
        var users = session.Advanced.RawQuery<User>(query).ToList();

        return users;
    }
}

// Attack: username = "' OR 1=1--"
// RQL injection

Why this is vulnerable:

  • String concatenation in RQL
  • No parameterization
  • Query injection
  • Data exfiltration

Secure Patterns

MongoDB with Type Validation

// SECURE - Strict type validation for MongoDB queries
using MongoDB.Driver;
using MongoDB.Bson;
using System;

public class SecureUserService
{
    private IMongoCollection<BsonDocument> _users;

    private string ValidateString(object value, int maxLength)
    {
        if (value == null)
        {
            throw new ArgumentException("Value cannot be null");
        }

        if (!(value is string strValue))
        {
            throw new ArgumentException("Expected string value");
        }

        if (strValue.Length > maxLength)
        {
            throw new ArgumentException($"Value exceeds max length {maxLength}");
        }

        return strValue;
    }

    public bool AuthenticateUser(string username, string password)
    {
        // SECURE - Validate input types
        var cleanUsername = ValidateString(username, 50);
        var cleanPassword = ValidateString(password, 100);

        // SECURE - Only string values, no operators
        var filter = Builders<BsonDocument>.Filter.And(
            Builders<BsonDocument>.Filter.Eq("username", cleanUsername),
            Builders<BsonDocument>.Filter.Eq("password", cleanPassword)
        );

        var user = _users.Find(filter).FirstOrDefault();
        return user != null;
    }
}

Why this works: Strict type checking prevents operator injection - the ValidateString() method ensures the input is actually a string type, rejecting BsonDocument objects or other types that could contain MongoDB operators like {"$ne": null}. Length limits prevent DoS attacks through extremely long input. Type-safe queries with validated string values cannot be manipulated to inject operators, as MongoDB will treat them as literal string comparisons rather than special query operators.

ASP.NET Core with Query Allowlist

// SECURE - Field allowlist and validation
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Bson;
using System.Collections.Generic;

[ApiController]
[Route("api/products")]
public class SecureProductController : ControllerBase
{
    private IMongoCollection<Product> _products;

    // SECURE - Define allowed query fields
    private static readonly Dictionary<string, Type> AllowedFields = new()
    {
        { "name", typeof(string) },
        { "category", typeof(string) },
        { "price_min", typeof(double) },
        { "price_max", typeof(double) }
    };

    private FilterDefinition<Product> BuildSafeFilter(Dictionary<string, object> parameters)
    {
        var builder = Builders<Product>.Filter;
        var filters = new List<FilterDefinition<Product>>();

        foreach (var (field, value) in parameters)
        {
            // SECURE - Only allow allowlisted fields
            if (!AllowedFields.ContainsKey(field))
            {
                continue;
            }

            var expectedType = AllowedFields[field];

            // SECURE - Validate type
            if (value?.GetType() != expectedType)
            {
                continue;
            }

            // SECURE - Build safe filter
            if (field == "price_min")
            {
                filters.Add(builder.Gte(p => p.Price, (double)value));
            }
            else if (field == "price_max")
            {
                filters.Add(builder.Lte(p => p.Price, (double)value));
            }
            else
            {
                filters.Add(builder.Eq(field, BsonValue.Create(value)));
            }
        }

        return filters.Count > 0 
            ? builder.And(filters) 
            : builder.Empty;
    }

    [HttpPost("search")]
    public IActionResult SearchProducts([FromBody] Dictionary<string, object> parameters)
    {
        var filter = BuildSafeFilter(parameters);
        var products = _products.Find(filter).Limit(100).ToList();

        return Ok(products);
    }
}

Why this works: Field allowlist (AllowedFields dictionary) restricts queries to explicitly permitted fields, preventing attackers from querying sensitive fields like admin_only or __proto__. Type validation ensures each field receives the expected data type (string vs double), blocking attempts to inject MongoDB operators or unexpected data structures. Controlled operator usage - the code explicitly chooses which MongoDB operators to use ($gte, $lte, $eq) based on field name, rather than accepting arbitrary operators from user input. Result limits (.Limit(100)) prevent DoS attacks from queries that would return massive datasets.

Strongly-Typed MongoDB Entities

// SECURE - Strongly-typed entities with LINQ
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using System.Text.RegularExpressions;

public class User
{
    public ObjectId Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
    public string Role { get; set; }
}

public class SecureUserRepository
{
    private IMongoCollection<User> _users;

    private static readonly Regex UsernamePattern = 
        new Regex(@"^[a-zA-Z0-9_]{3,50}$", RegexOptions.Compiled);

    private string ValidateUsername(string username)
    {
        if (string.IsNullOrEmpty(username))
        {
            throw new ArgumentException("Username cannot be empty");
        }

        if (!UsernamePattern.IsMatch(username))
        {
            throw new ArgumentException("Invalid username format");
        }

        return username;
    }

    public User GetUser(string username)
    {
        var cleanUsername = ValidateUsername(username);

        // SECURE - Type-safe LINQ query
        return _users.AsQueryable()
            .Where(u => u.Username == cleanUsername)
            .FirstOrDefault();
    }

    public List<User> GetUsersByRole(string role)
    {
        var allowedRoles = new[] { "user", "moderator", "admin" };

        if (!allowedRoles.Contains(role))
        {
            throw new ArgumentException("Invalid role");
        }

        // SECURE - Type-safe query with validated input
        return _users.AsQueryable()
            .Where(u => u.Role == role)
            .ToList();
    }
}

Why this works: Strongly-typed entities (IMongoCollection<User> instead of BsonDocument) ensure compile-time type safety - the compiler verifies all property accesses and comparisons match the User class definition, preventing field name typos and type mismatches. LINQ queries are translated to safe MongoDB queries by the driver, avoiding string concatenation and operator injection. Input validation (regex pattern matching, allowlist checking) ensures only expected values reach the database. Type checking guarantees parameters are the correct type before use.

Azure Cosmos DB with Parameterization

// SECURE - Cosmos DB with parameterized queries
using Microsoft.Azure.Cosmos;

public class SecureCosmosService
{
    private Container _container;

    private static readonly string[] AllowedRoles = { "user", "moderator", "admin" };

    private string ValidateRole(string role)
    {
        if (!AllowedRoles.Contains(role))
        {
            throw new ArgumentException("Invalid role");
        }

        return role;
    }

    public async Task<List<User>> SearchUsers(string role)
    {
        // SECURE - Validate input
        var cleanRole = ValidateRole(role);

        // SECURE - Parameterized query
        var queryDefinition = new QueryDefinition(
            "SELECT * FROM c WHERE c.role = @role")
            .WithParameter("@role", cleanRole);

        var query = _container.GetItemQueryIterator<User>(queryDefinition);
        var results = new List<User>();

        while (query.HasMoreResults)
        {
            var response = await query.ReadNextAsync();
            results.AddRange(response);
        }

        return results;
    }
}

Why this works: Parameterized queries (QueryDefinition with .WithParameter()) separate SQL code from data - the @role placeholder is treated as a literal value, not executable SQL, preventing SQL injection even if the role contains quotes or SQL keywords. Input validation (allowlist checking) provides defense-in-depth by rejecting invalid roles before they reach the database. No string concatenation eliminates the attack surface for injection - unlike $"SELECT * FROM c WHERE c.role = '{role}'", the parameterized version cannot be manipulated by special characters in the role value.

Redis with Input Validation

// SECURE - Redis with key and value validation
using StackExchange.Redis;
using Microsoft.AspNetCore.Mvc;
using System.Text.RegularExpressions;

[ApiController]
[Route("cache")]
public class SecureCacheController : ControllerBase
{
    private IDatabase _redis;

    private static readonly Regex KeyPattern = 
        new Regex(@"^[a-zA-Z0-9_-]{1,100}$", RegexOptions.Compiled);

    private string ValidateRedisKey(string key)
    {
        if (string.IsNullOrEmpty(key))
        {
            throw new ArgumentException("Key cannot be empty");
        }

        // SECURE - Only allow alphanumeric, dash, underscore
        if (!KeyPattern.IsMatch(key))
        {
            throw new ArgumentException("Invalid key format");
        }

        return key;
    }

    private string ValidateRedisValue(string value)
    {
        if (value == null)
        {
            throw new ArgumentException("Value cannot be null");
        }

        // SECURE - Remove CRLF to prevent command injection
        var cleanValue = value.Replace("\r", "").Replace("\n", "");

        if (cleanValue.Length > 10000)
        {
            throw new ArgumentException("Value too large");
        }

        return cleanValue;
    }

    [HttpGet("{key}")]
    public IActionResult GetCache(string key)
    {
        try
        {
            var cleanKey = ValidateRedisKey(key);
            var value = _redis.StringGet(cleanKey);
            return Ok(value.ToString());
        }
        catch (ArgumentException ex)
        {
            return BadRequest(ex.Message);
        }
    }

    [HttpPost]
    public IActionResult SetCache([FromBody] CacheRequest request)
    {
        try
        {
            var cleanKey = ValidateRedisKey(request.Key);
            var cleanValue = ValidateRedisValue(request.Value);

            // SECURE - Use setex with expiration
            _redis.StringSet(cleanKey, cleanValue, TimeSpan.FromHours(1));

            return Ok();
        }
        catch (ArgumentException ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

public class CacheRequest
{
    public string Key { get; set; }
    public string Value { get; set; }
}

Why this works: Regex validation (KeyPattern) enforces an allowlist of safe characters (alphanumeric, dash, underscore), blocking special characters that could be used in injection attacks. CRLF removal (Replace("\r", "").Replace("\n", "")) prevents command injection attempts where attackers inject newlines to execute additional Redis commands. Length limits prevent DoS attacks from extremely long keys or values. Automatic expiration (TimeSpan.FromHours(1)) limits the lifespan of cached data. StackExchange.Redis provides additional protection by using binary protocol and preventing command injection in most cases.

Regex Escaping

// SECURE - MongoDB regex with proper escaping
using MongoDB.Driver;
using System.Text.RegularExpressions;

public class SecureSearchService
{
    private IMongoCollection<User> _users;

    private string EscapeRegex(string input)
    {
        // SECURE - Escape special regex characters
        return Regex.Escape(input);
    }

    private string ValidateSearchTerm(string searchTerm)
    {
        if (string.IsNullOrEmpty(searchTerm))
        {
            throw new ArgumentException("Search term cannot be empty");
        }

        if (searchTerm.Length > 100)
        {
            throw new ArgumentException("Search term too long");
        }

        return searchTerm;
    }

    public List<User> SearchUsers(string searchTerm)
    {
        // SECURE - Validate input
        var cleanTerm = ValidateSearchTerm(searchTerm);

        // SECURE - Escape regex special characters
        var escapedTerm = EscapeRegex(cleanTerm);

        var filter = Builders<User>.Filter.Regex(
            u => u.Username,
            new BsonRegularExpression(escapedTerm, "i")
        );

        return _users.Find(filter).Limit(100).ToList();
    }
}

Why this works: Regex.Escape() neutralizes regex special characters - patterns like .* (match everything) become \.\* (literal period and asterisk), preventing attackers from crafting patterns that match all documents or cause ReDoS (Regular Expression Denial of Service) attacks. Length limits prevent excessively long search terms that could cause performance issues. Input validation ensures the search term exists and is reasonable. Result limits (.Limit(100)) cap the number of returned documents even if the regex matches many records.

Verification

To verify NoSQL injection protection:

  • Test operator injection: Attempt to pass MongoDB operators (e.g., $ne, $gt, $where) in query parameters and verify they are rejected or sanitized
  • Verify type checking: Ensure query parameters are validated for expected types (string values cannot be objects)
  • Test field allowlist: Confirm queries only accept explicitly permitted fields and reject unknown fields
  • Check for $where usage: Search codebase for $where operator and ensure it's not used with user input
  • Test with malicious payloads: Try injection attempts like { "$ne": null } for authentication bypass
  • Review query builders: Verify all queries use typed entities or properly validated filter definitions
  • Test boundary conditions: Attempt extreme values, null values, and unexpected types in all query parameters
using Xunit;
using MongoDB.Bson;

public class NoSQLInjectionTests
{
    [Fact]
    public void TestOperatorInjectionBlocked()
    {
        var service = new SecureUserService();

        // Should throw exception (not a string)
        var bsonDoc = new BsonDocument("$ne", BsonNull.Value);

        Assert.Throws<ArgumentException>(() =>
            service.AuthenticateUser(bsonDoc, "password")
        );
    }

    [Fact]
    public void TestFieldAllowlistEnforced()
    {
        var controller = new SecureProductController();

        var parameters = new Dictionary<string, object>
        {
            { "name", "test" },
            { "admin_only", true },  // Not in allowlist
            { "__proto__", "malicious" }
        };

        var filter = controller.BuildSafeFilter(parameters);

        // Should only include allowlisted fields
        var filterString = filter.ToString();
        Assert.DoesNotContain("admin_only", filterString);
        Assert.DoesNotContain("__proto__", filterString);
    }

    [Fact]
    public void TestRegexEscaping()
    {
        var service = new SecureSearchService();

        // Should escape special characters
        var escaped = service.EscapeRegex(".*");
        Assert.Equal(@"\.\*", escaped);
    }
}

Additional Resources