Skip to content

CWE-943: NoSQL Injection - Java

Overview

NoSQL Injection in Java applications occurs when untrusted input is used to construct NoSQL database queries (MongoDB, Redis, Cassandra, DynamoDB, 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 Spring Data MongoDB's Query and Criteria API or MongoDB Java driver's Filters class instead of string concatenation, validate and type-check all user input before including in queries, implement allowlists for query operators ($eq, $gt, etc.) and field names, and use strongly-typed repositories with method query derivation to prevent NoSQL injection attacks.

Common Java NoSQL Vulnerabilities:

  • MongoDB query operator injection using BSON documents
  • Spring Data MongoDB query injection
  • Redis command injection via Jedis/Lettuce
  • Cassandra CQL injection
  • DynamoDB expression injection

Popular Java NoSQL Libraries:

  • MongoDB Java Driver: Official MongoDB driver
  • Spring Data MongoDB: Spring framework MongoDB integration
  • Morphia: MongoDB ODM for Java
  • Jedis / Lettuce: Redis clients
  • DataStax Java Driver: Cassandra driver
  • AWS SDK: DynamoDB client

Common Vulnerable Patterns

MongoDB Operator Injection

// VULNERABLE - Direct untrusted input in MongoDB query
import com.mongodb.client.*;
import org.bson.Document;

public class UserService {
    private MongoCollection<Document> users;

    public boolean authenticateUser(String username, Object password) {
        // VULNERABLE - Accepting Object type allows operator injection
        Document query = new Document("username", username)
                            .append("password", password);

        Document user = users.find(query).first();
        return user != null;
    }
}

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

Why this is vulnerable:

  • Accepts Object type (can be Document with operators)
  • No type validation
  • MongoDB operators injectable
  • Authentication bypass

Spring Data MongoDB Injection

// VULNERABLE - Spring Data with raw query
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.web.bind.annotation.*;

@RestController
public class ProductController {

    private final MongoTemplate mongoTemplate;

    @PostMapping("/api/products/search")
    public List<Product> searchProducts(@RequestBody Map<String, Object> queryMap) {
        // VULNERABLE - Untrusted query map
        BasicQuery query = new BasicQuery(new Document(queryMap));

        return mongoTemplate.find(query, Product.class);
    }
}

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

Why this is vulnerable:

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

MongoDB $where Injection

// VULNERABLE - JavaScript code injection via $where
import com.mongodb.client.*;
import org.bson.Document;

public class UserRepository {
    private MongoCollection<Document> users;

    public List<Document> findUsersByAge(String minAge) {
        // VULNERABLE - String concatenation in $where
        String whereClause = "this.age > " + minAge;
        Document query = new Document("$where", whereClause);

        return users.find(query).into(new ArrayList<>());
    }
}

// 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

Morphia with Unsafe Queries

// VULNERABLE - Morphia ODM with raw queries
import dev.morphia.Datastore;
import dev.morphia.query.Query;
import org.bson.Document;

@Entity("users")
public class User {
    @Id private ObjectId id;
    private String username;
    private String role;
    // getters/setters
}

public class UserService {
    private Datastore datastore;

    public User findUser(Map<String, Object> criteria) {
        // VULNERABLE - Untrusted criteria
        Query<User> query = datastore.find(User.class);

        for (Map.Entry<String, Object> entry : criteria.entrySet()) {
            // No validation on field names or operators
            query.filter(entry.getKey(), entry.getValue());
        }

        return query.first();
    }
}

// Attack: criteria = {"role": {"$ne": "user"}}
// Returns admin user

Why this is vulnerable:

  • No field validation
  • Operator injection
  • Privilege escalation
  • ODM bypassed

Redis Command Injection

// VULNERABLE - Redis with unsanitized keys
import redis.clients.jedis.Jedis;
import org.springframework.web.bind.annotation.*;

@RestController
public class CacheController {
    private Jedis jedis = new Jedis("localhost");

    @GetMapping("/cache/{key}")
    public String getCache(@PathVariable String key) {
        // VULNERABLE - Untrusted input in Redis key
        return jedis.get(key);
    }

    @PostMapping("/cache")
    public String setCache(@RequestParam String key, 
                          @RequestParam String value) {
        // VULNERABLE - CRLF injection possible
        jedis.set(key, value);
        return "OK";
    }
}

// Attack: key = "test\r\nFLUSHDB\r\n"
// Injects Redis command to flush database

Why this is vulnerable:

  • No key validation
  • CRLF injection
  • Command injection
  • Database wipeout

MongoDB Aggregation Injection

// VULNERABLE - Aggregation pipeline with untrusted input
import com.mongodb.client.*;
import org.bson.Document;
import java.util.*;

public class AnalyticsService {
    private MongoCollection<Document> events;

    public List<Document> getUserStats(String userId, String sortField) {
        // VULNERABLE - Untrusted input in aggregation pipeline
        List<Document> pipeline = Arrays.asList(
            new Document("$match", new Document("user_id", userId)),
            new Document("$sort", new Document(sortField, -1)),
            new Document("$limit", 10)
        );

        return events.aggregate(pipeline).into(new ArrayList<>());
    }
}

// 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
import com.mongodb.client.*;
import org.bson.Document;
import java.util.regex.Pattern;

public class SearchService {
    private MongoCollection<Document> users;

    public List<Document> searchUsers(String searchTerm) {
        // VULNERABLE - Untrusted input in regex without escaping
        Document query = new Document("username", 
            new Document("$regex", searchTerm)
                .append("$options", "i"));

        return users.find(query).into(new ArrayList<>());
    }
}

// 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

DynamoDB Expression Injection

// VULNERABLE - DynamoDB with untrusted expressions
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.document.*;
import com.amazonaws.services.dynamodbv2.document.spec.ScanSpec;

public class DynamoService {
    private Table table;

    public List<Item> searchUsers(String attributeName, String value) {
        // VULNERABLE - Untrusted attribute name
        ScanSpec scanSpec = new ScanSpec()
            .withFilterExpression(attributeName + " = :val")
            .withValueMap(Map.of(":val", value));

        return table.scan(scanSpec).iterator().next();
    }
}

// Attack: attributeName = "admin_flag", value = "true"
// Accesses hidden attributes

Why this is vulnerable:

  • Attribute names from untrusted sources
  • No field allowlist
  • Hidden attributes accessible
  • Authorization bypass

Secure Patterns

MongoDB with Type Validation

// SECURE - Strict type validation for MongoDB queries
import com.mongodb.client.*;
import org.bson.Document;

public class SecureUserService {
    private MongoCollection<Document> users;

    private String validateString(String value, int maxLength) {
        if (value == null) {
            throw new IllegalArgumentException("Value cannot be null");
        }

        if (!(value instanceof String)) {
            throw new IllegalArgumentException("Expected string value");
        }

        if (value.length() > maxLength) {
            throw new IllegalArgumentException(
                "Value exceeds max length " + maxLength);
        }

        return value;
    }

    public boolean authenticateUser(String username, String password) {
        // SECURE - Validate input types
        String cleanUsername = validateString(username, 50);
        String cleanPassword = validateString(password, 100);

        // SECURE - Only string values, no operators
        Document query = new Document("username", cleanUsername)
                            .append("password", cleanPassword);

        Document user = users.find(query).first();
        return user != null;
    }
}

Why this works: This pattern prevents NoSQL injection through strict type validation and defensive programming. By accepting only String parameters and explicitly checking types with instanceof, the code rejects MongoDB operator objects like {"$ne": null} that attackers use to bypass authentication. The validateString method enforces maximum length limits (preventing DoS attacks with massive inputs) and confirms the input is genuinely a String, not a BSON Document containing query operators. The query construction uses only validated String values in a simple equality comparison new Document("username", cleanUsername), which MongoDB treats as literal string matching - no operators like $ne, $gt, or $where can be injected. This defense-in-depth approach ensures even if an attacker controls the input, they cannot manipulate the query structure or logic.

Spring Data with Query Allowlist

// SECURE - Spring Data with field allowlist
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.web.bind.annotation.*;
import java.util.*;

@RestController
public class SecureProductController {

    private final MongoTemplate mongoTemplate;

    // SECURE - Define allowed query fields
    private static final Map<String, Class<?>> ALLOWED_FIELDS = Map.of(
        "name", String.class,
        "category", String.class,
        "price_min", Number.class,
        "price_max", Number.class
    );

    private Query buildSafeQuery(Map<String, Object> params) {
        Query query = new Query();
        Criteria criteria = new Criteria();

        for (Map.Entry<String, Object> entry : params.entrySet()) {
            String field = entry.getKey();
            Object value = entry.getValue();

            // SECURE - Only allow allowlisted fields
            if (!ALLOWED_FIELDS.containsKey(field)) {
                continue;
            }

            Class<?> expectedType = ALLOWED_FIELDS.get(field);

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

            // SECURE - Build safe criteria
            if (field.equals("price_min")) {
                criteria = criteria.and("price").gte(value);
            } else if (field.equals("price_max")) {
                criteria = criteria.and("price").lte(value);
            } else {
                criteria = criteria.and(field).is(value);
            }
        }

        query.addCriteria(criteria);
        query.limit(100);

        return query;
    }

    @PostMapping("/api/products/search")
    public List<Product> searchProducts(@RequestBody Map<String, Object> params) {
        Query safeQuery = buildSafeQuery(params);
        return mongoTemplate.find(safeQuery, Product.class);
    }
}

Why this works: The field allowlist approach prevents attackers from querying arbitrary database fields or injecting operators. The ALLOWED_FIELDS set acts as an allow-list, rejecting any query parameters not explicitly permitted (like admin_only or internal fields). Type validation ensures numeric fields receive only numbers - preventing operator injection through type confusion. Controlled operator usage means the code explicitly constructs safe queries using Spring Data's Criteria.where() API with typed methods like gte() and lte(), which prevent operator injection because they're programmatic, not string-based. The limit(100) prevents resource exhaustion attacks. This pattern gives users controlled query capability without exposing the application to injection attacks, because the query structure is defined by the code, not user input.

Spring Data Repository (Type-Safe)

// SECURE - Spring Data Repository with type safety
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;

@Document(collection = "users")
public class User {
    @Id
    private String id;

    @Field("username")
    @Indexed(unique = true)
    private String username;

    @Field("email")
    private String email;

    @Field("role")
    private String role;

    // getters/setters
}

@Repository
public interface UserRepository extends MongoRepository<User, String> {

    // SECURE - Type-safe repository methods
    User findByUsername(String username);

    List<User> findByRole(String role);

    @Query("{ 'email': ?0 }")
    User findByEmail(String email);
}

@Service
public class SecureUserService {

    private final UserRepository userRepository;

    private static final Pattern USERNAME_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9_]{3,50}$");

    private String validateUsername(String username) {
        if (username == null || username.isEmpty()) {
            throw new IllegalArgumentException("Username cannot be empty");
        }

        if (!USERNAME_PATTERN.matcher(username).matches()) {
            throw new IllegalArgumentException("Invalid username format");
        }

        return username;
    }

    public User getUser(String username) {
        String cleanUsername = validateUsername(username);

        // SECURE - Type-safe repository method
        return userRepository.findByUsername(cleanUsername);
    }
}

Why this works: Spring Data Repository methods provide type-safe query construction through method names and annotations. Methods like findByUsernameAndEmail() are parsed at compile-time into parameterized queries - the framework handles query construction safely without string concatenation. The @Query annotation with ?0 placeholders ensures parameters are bound safely, similar to prepared statements in SQL. The @Document class provides schema validation, enforcing that only defined fields exist and have correct types. Because the repository interface defines the query structure, attackers cannot inject operators or modify query logic - they can only provide values for predefined parameters. This compile-time safety eliminates an entire class of injection vulnerabilities.

Redis with Input Validation

// SECURE - Redis with key and value validation
import redis.clients.jedis.Jedis;
import org.springframework.web.bind.annotation.*;
import java.util.regex.Pattern;

@RestController
public class SecureCacheController {

    private Jedis jedis = new Jedis("localhost");

    private static final Pattern KEY_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9_-]{1,100}$");

    private String validateRedisKey(String key) {
        if (key == null || key.isEmpty()) {
            throw new IllegalArgumentException("Key cannot be empty");
        }

        // SECURE - Only allow alphanumeric, dash, underscore
        if (!KEY_PATTERN.matcher(key).matches()) {
            throw new IllegalArgumentException("Invalid key format");
        }

        return key;
    }

    private String validateRedisValue(String value) {
        if (value == null) {
            throw new IllegalArgumentException("Value cannot be null");
        }

        // SECURE - Remove CRLF to prevent command injection
        String cleanValue = value.replaceAll("[\\r\\n]", "");

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

        return cleanValue;
    }

    @GetMapping("/cache/{key}")
    public String getCache(@PathVariable String key) {
        String cleanKey = validateRedisKey(key);
        String value = jedis.get(cleanKey);
        return value != null ? value : "Not found";
    }

    @PostMapping("/cache")
    public String setCache(@RequestParam String key, 
                          @RequestParam String value) {
        String cleanKey = validateRedisKey(key);
        String cleanValue = validateRedisValue(value);

        // SECURE - Use setex with expiration
        jedis.setex(cleanKey, 3600, cleanValue);
        return "OK";
    }
}

Why this works: Redis command injection exploits CRLF characters (\r\n) in the Redis protocol to inject additional commands. The validateKey method uses a strict regex pattern ^[a-zA-Z0-9:_-]+$ allowing only alphanumeric characters, colons, underscores, and hyphens - preventing CRLF injection. The sanitizeValue method explicitly removes \r, \n, and control characters that could be used for protocol injection. Length limits (200 characters for keys, 10KB for values) prevent DoS attacks. By validating both keys and values before any Redis operation, the code ensures that user input cannot break out of the intended command structure to execute arbitrary Redis commands like FLUSHDB or CONFIG.

Safe MongoDB Aggregation

// SECURE - MongoDB aggregation with field allowlist
import com.mongodb.client.*;
import org.bson.Document;
import java.util.*;
import java.util.regex.Pattern;

public class SecureAnalyticsService {

    private MongoCollection<Document> events;

    // SECURE - Define allowed sort fields
    private static final Set<String> ALLOWED_SORT_FIELDS = 
        Set.of("timestamp", "event_type", "user_id");

    private static final Pattern USER_ID_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9_-]{1,50}$");

    private String validateUserId(String userId) {
        if (userId == null || userId.isEmpty()) {
            throw new IllegalArgumentException("User ID cannot be empty");
        }

        if (!USER_ID_PATTERN.matcher(userId).matches()) {
            throw new IllegalArgumentException("Invalid user ID format");
        }

        return userId;
    }

    public List<Document> getUserStats(String userId, String sortField) {
        // SECURE - Validate user ID
        String cleanUserId = validateUserId(userId);

        // SECURE - Validate sort field against allowlist
        if (!ALLOWED_SORT_FIELDS.contains(sortField)) {
            throw new IllegalArgumentException(
                "Invalid sort field. Allowed: " + ALLOWED_SORT_FIELDS);
        }

        // SECURE - Build pipeline with validated values
        List<Document> pipeline = Arrays.asList(
            new Document("$match", new Document("user_id", cleanUserId)),
            new Document("$sort", new Document(sortField, -1)),
            new Document("$limit", 100)
        );

        return events.aggregate(pipeline).into(new ArrayList<>());
    }
}

Why this works: MongoDB aggregation pipelines are powerful but dangerous when constructed with untrusted input. The ALLOWED_SORT_FIELDS set prevents injection of dangerous operators like $where (which executes JavaScript) or $lookup (which can access other collections). The validateUserId method ensures the user ID matches expected format, rejecting operator objects. By constructing the aggregation pipeline programmatically with validated inputs in controlled positions, the code prevents attackers from adding malicious pipeline stages. The limit(1000) prevents resource exhaustion. This approach allows legitimate aggregation queries while ensuring the pipeline structure remains under application control, not attacker control.

Regex Escaping

// SECURE - MongoDB regex with proper escaping
import com.mongodb.client.*;
import org.bson.Document;
import java.util.*;
import java.util.regex.Pattern;

public class SecureSearchService {

    private MongoCollection<Document> users;

    private String escapeRegex(String input) {
        // SECURE - Escape special regex characters
        return input.replaceAll("[.*+?^${}()|\\[\\]\\\\]", "\\\\$0");
    }

    private String validateSearchTerm(String searchTerm) {
        if (searchTerm == null || searchTerm.isEmpty()) {
            throw new IllegalArgumentException("Search term cannot be empty");
        }

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

        return searchTerm;
    }

    public List<Document> searchUsers(String searchTerm) {
        // SECURE - Validate input
        String cleanTerm = validateSearchTerm(searchTerm);

        // SECURE - Escape regex special characters
        String escapedTerm = escapeRegex(cleanTerm);

        Document query = new Document("username", 
            new Document("$regex", escapedTerm)
                .append("$options", "i"));

        return users.find(query).limit(100).into(new ArrayList<>());
    }
}

Why this works: MongoDB regex queries can be exploited through specially crafted patterns causing ReDoS (Regular Expression Denial of Service) or information disclosure. The Pattern.quote() method escapes all regex metacharacters (.*+?[]{}()^$|\), treating the user input as a literal string - preventing attackers from injecting patterns like .* (match everything) or catastrophic backtracking patterns like (a+)+. The case-insensitive flag provides user-friendly search without security risk. Length validation (100 characters) prevents extremely long patterns that could cause DoS. The limit(100) on results prevents resource exhaustion. This pattern enables safe text search functionality where user input is treated as a literal search term, not a regex pattern under attacker control.

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Additional Resources