NoSQL Injection

ID

swift.nosql_injection

Severity

critical

Resource

Injection

Language

Swift

Tags

CWE:943, NIST.SP.800-53, OWASP:2021:A3, PCI-DSS:6.5.1

Description

Improper Neutralization of Special Elements in Data Query Logic ("NoSQL Injection")

NoSQL Injection exploits weaknesses in applications interacting with NoSQL databases, such as MongoDB or Couchbase, by manipulating input data to alter query execution.

Unlike traditional SQL injections, which rely on SQL syntax, NoSQL injections leverage the flexible, schema-less nature of NoSQL databases.

Rationale

NoSQL databases often provide "query-by-example" capabilities, allowing users to construct queries from JSON without the need for SQL-like syntax. Unfortunately, this flexibility comes with the potential for NoSQL Injection vulnerabilities.

For example, a MongoDB query such as

db.users.find({ username: username, password: password })

could be altered using query and projection operators: username={"gt": ""} and `password={"gt": ""} match to mach any document.

Consider a Swift iOS application using MongoDB (MongoSwift) to authenticate users:

import MongoSwift
import Foundation

class UserService {
    let client: MongoClient
    let database: MongoDatabase
    let users: MongoCollection<BsonDocument>

    init(connectionString: String) throws {
        self.client = try MongoClient(connectionString)
        self.database = client.db("testdb")
        self.users = database.collection("users")
    }

    // VULNERABLE: directly constructing query from user input
    func authenticateUser(username: String, password: String) throws -> BsonDocument? {
        // User input directly embedded in BSON document
        let queryString = """
        {
            "username": "\(username)",
            "password": "\(password)"
        }
        """

        let query = try BsonDocument(fromJSON: queryString)
        return try users.findOne(query)
    }
}

// Example usage with malicious input:
// username: admin", "$or": [{"a": "a
// password: "}} ]
// Resulting query: {"username": "admin", "$or": [{"a": "a", "password": "}} ]"}
// This bypasses authentication!

In this scenario, if username or password are not properly validated, an attacker can inject MongoDB operators to manipulate the query. For instance:

  • Setting username to {"$ne": null} could match any username

  • Using $or, $and, $where operators to alter query logic

  • Injecting $regex patterns for information disclosure

Another vulnerable pattern with MongoKitten:

import MongoKitten

class ProductService {
    let database: MongoDatabase

    // VULNERABLE: using user input in raw commands
    func findProducts(category: String) async throws -> [Document] {
        // User input embedded in raw command
        let commandString = """
        {
            "find": "products",
            "filter": { "category": "\(category)" }
        }
        """

        let command = try Document(fromJSON: commandString)
        let result = try await database.runCommand(command)
        return result["cursor"]["firstBatch"].arrayValue ?? []
    }
}

CouchbaseLite is also vulnerable:

import CouchbaseLiteSwift

class DocumentService {
    let database: Database

    init() throws {
        self.database = try Database(name: "mydb")
    }

    // VULNERABLE: using user input in query expressions
    func searchDocuments(searchTerm: String) throws -> [Result] {
        // User input directly used in query string
        let queryString = "SELECT * FROM products WHERE name = '\(searchTerm)'"
        let query = database.createQuery(queryString)
        return try query.execute()
    }
}

Remediation

The previous vulnerabilities can be fixed by using parameterized queries and proper input validation:

MongoDB (MongoSwift) - Fixed:

import MongoSwift
import Foundation

class UserService {
    let users: MongoCollection<BsonDocument>

    // FIXED: use structured query with type-safe parameters
    func authenticateUser(username: String, password: String) throws -> BsonDocument? {
        // Validate inputs first
        guard isValidUsername(username), isValidPassword(password) else {
            throw ValidationError.invalidInput
        }

        // Use structured query builder - no string interpolation
        let query: BsonDocument = [
            "username": .string(username),
            "password": .string(password)
        ]

        return try users.findOne(query)
    }

    private func isValidUsername(_ username: String) -> Bool {
        // Only allow alphanumeric and basic punctuation
        let pattern = "^[a-zA-Z0-9._-]{3,50}$"
        return username.range(of: pattern, options: .regularExpression) != nil
    }

    private func isValidPassword(_ password: String) -> Bool {
        // Validate password format
        return password.count >= 8 && password.count <= 128
    }
}

MongoKitten - Fixed:

import MongoKitten

class ProductService {
    let collection: MongoCollection

    // FIXED: use query builder with proper parameter binding
    func findProducts(category: String) async throws -> [Document] {
        // Validate input
        guard isValidCategory(category) else {
            throw ValidationError.invalidCategory
        }

        // Use query builder - parameters are properly escaped
        let query: Document = ["category": category]
        return try await collection.find(query).drain()
    }

    private func isValidCategory(_ category: String) -> Bool {
        // Whitelist allowed categories
        let allowedCategories = ["electronics", "books", "clothing", "food"]
        return allowedCategories.contains(category.lowercased())
    }
}

CouchbaseLite - Fixed:

import CouchbaseLiteSwift

class DocumentService {
    let database: Database

    // FIXED: use QueryBuilder with parameterized queries
    func searchDocuments(searchTerm: String) throws -> [Result] {
        // Validate input
        guard isValidSearchTerm(searchTerm) else {
            throw ValidationError.invalidSearchTerm
        }

        // Use QueryBuilder with Expression API
        let query = QueryBuilder
            .select(SelectResult.all())
            .from(DataSource.database(database))
            .where(Expression.property("name").equalTo(Expression.string(searchTerm)))

        return try query.execute().allResults()
    }

    private func isValidSearchTerm(_ term: String) -> Bool {
        // Validate search term format
        return !term.isEmpty && term.count <= 100
    }
}

Best Practices for Swift NoSQL Security:

import Foundation

// Input validation utilities
class NoSQLInputValidator {

    // Reject MongoDB operators in user input
    static func sanitizeForMongoDB(_ input: String) -> String? {
        // Reject strings containing MongoDB operators
        let dangerousPatterns = ["$", "{", "}", "[", "]"]
        for pattern in dangerousPatterns {
            if input.contains(pattern) {
                return nil
            }
        }
        return input
    }

    // Whitelist validation for enum-like fields
    static func validateCategory(_ category: String, allowedCategories: [String]) -> Bool {
        return allowedCategories.contains(category)
    }

    // Type-safe query builder
    static func buildSafeQuery(field: String, value: Any) -> BsonDocument {
        // Ensure field names are from a whitelist
        guard isValidFieldName(field) else {
            fatalError("Invalid field name: \(field)")
        }

        // Use type-safe construction
        switch value {
        case let str as String:
            return [field: .string(str)]
        case let int as Int:
            return [field: .int32(Int32(int))]
        case let bool as Bool:
            return [field: .bool(bool)]
        default:
            return [:]
        }
    }

    private static func isValidFieldName(_ field: String) -> Bool {
        // Whitelist of allowed field names
        let allowedFields = ["username", "email", "category", "status", "name"]
        return allowedFields.contains(field)
    }
}

To mitigate NoSQL Injection risks in applications, follow these best practices:

  1. Input Validation: Rigorously validate inputs before processing them in NoSQL queries. Apply strict checks on data types, lengths, and formats.

  2. Use of Binding Mechanisms: Prefer using binding mechanisms or parameterized query structures provided by the NoSQL database’s library when assembling queries. This approach helps separate data from query logic.

  3. Sanitization: When using user inputs in query objects, ensure they are sanitized and free from characters or patterns that could be interpreted as part of a query structure. This is hard to do and error-prone, so consider first using binding mechanisms for parameterized queries, when available.

  4. Whitelist Validation: Implement whitelisting for input validation to ensure only expected values are allowed and unexpected input is rejected.

  5. Access Controls and Principle of Least Privilege: Implement strict access controls at the database level, and ensure that applications access only permitted data. Use credentials with the least privileges needed for the functionality.

  6. Regular Security Audits and SAST: Regularly audit your codebase for security vulnerabilities and include SAST tools in your development pipeline to catch potential NoSQL injections early in the development process.

By adopting these measures, you reduce the risk of NoSQL Injection vulnerabilities in your applications, protecting your database’s integrity and confidentiality.

Configuration

The detector has the following configurable parameters:

  • sources, that indicates the source kinds to check.

  • neutralizations, that indicates the neutralization kinds to check.

Unless you need to change the default behavior, you typically do not need to configure this detector.