NoSQL Injection Vulnerabilities

High Risk Input Validation
nosqlmongodbcouchdbinjectionjavascript-injectionjson-manipulationdatabase

What it is

NoSQL injection attacks exploit vulnerabilities in NoSQL databases like MongoDB, CouchDB, Redis, and Cassandra by injecting malicious payloads into database queries. Unlike traditional SQL injection, NoSQL injection can involve JavaScript code execution, JSON manipulation, or operator injection depending on the database type and query structure.

const express = require('express');
const { MongoClient } = require('mongodb');

app.post('/search', async (req, res) => {
    const { username, minAge } = req.body;
    
    // VULNERABLE: JavaScript injection via $where
    const query = {
        $where: `this.username == '${username}' && this.age >= ${minAge}`
    };
    
    const users = await db.collection('users').find(query).toArray();
    res.json(users);
});

// Malicious input:
// username: "'; return true; //"
// minAge: "0"
// Executes: this.username == ''; return true; //' && this.age >= 0
const express = require('express');
const { MongoClient } = require('mongodb');

app.post('/search', async (req, res) => {
    try {
        const { username, minAge } = req.body;
        
        // Input validation
        if (!username || typeof username !== 'string' || username.length > 50) {
            return res.status(400).json({ error: 'Invalid username' });
        }
        
        const ageNum = parseInt(minAge);
        if (isNaN(ageNum) || ageNum < 0 || ageNum > 150) {
            return res.status(400).json({ error: 'Invalid age' });
        }
        
        // SECURE: Using native MongoDB operators instead of $where
        const query = {
            username: username,  // Exact match, no injection possible
            age: { $gte: ageNum }
        };
        
        // Additional security: limit results and use projection
        const users = await db.collection('users')
            .find(query, { projection: { password: 0, _id: 0 } })
            .limit(100)
            .toArray();
            
        res.json(users);
    } catch (error) {
        console.error('Search error:', error);
        res.status(500).json({ error: 'Search failed' });
    }
});

💡 Why This Fix Works

The vulnerable code uses $where with string concatenation, allowing JavaScript injection. The secure version uses native MongoDB operators and proper input validation.

const express = require('express');
const bcrypt = require('bcrypt');

app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    
    // VULNERABLE: Direct use of user input as query object
    const user = await db.collection('users').findOne({
        username: username,  // Can be {"$ne": ""} to match any user
        password: password   // Can bypass password checking
    });
    
    if (user) {
        res.json({ message: 'Login successful', userId: user._id });
    } else {
        res.status(401).json({ message: 'Invalid credentials' });
    }
});

// Malicious POST body:
// {
//   "username": {"$ne": ""},
//   "password": {"$ne": ""}
// }
// Bypasses authentication by matching any user with non-empty credentials
const express = require('express');
const bcrypt = require('bcrypt');

app.post('/login', async (req, res) => {
    try {
        const { username, password } = req.body;
        
        // SECURE: Strict type validation
        if (typeof username !== 'string' || typeof password !== 'string') {
            return res.status(400).json({ message: 'Invalid input types' });
        }
        
        // Length validation
        if (username.length === 0 || username.length > 50 ||
            password.length === 0 || password.length > 100) {
            return res.status(400).json({ message: 'Invalid input length' });
        }
        
        // Format validation for username
        if (!/^[a-zA-Z0-9._-]+$/.test(username)) {
            return res.status(400).json({ message: 'Invalid username format' });
        }
        
        // SECURE: Explicit string-only query
        const user = await db.collection('users').findOne({
            username: { $eq: username }  // Explicit equality operator
        });
        
        if (!user) {
            return res.status(401).json({ message: 'Invalid credentials' });
        }
        
        // Verify password using bcrypt
        const isPasswordValid = await bcrypt.compare(password, user.password);
        
        if (isPasswordValid) {
            // Generate secure session token
            const sessionToken = generateSecureToken();
            
            // Store session with expiration
            await db.collection('sessions').insertOne({
                userId: user._id,
                token: sessionToken,
                createdAt: new Date(),
                expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
            });
            
            res.json({ 
                message: 'Login successful', 
                token: sessionToken 
            });
        } else {
            res.status(401).json({ message: 'Invalid credentials' });
        }
    } catch (error) {
        console.error('Login error:', error);
        res.status(500).json({ message: 'Login failed' });
    }
});

function generateSecureToken() {
    return require('crypto').randomBytes(32).toString('hex');
}

💡 Why This Fix Works

The vulnerable code accepts objects as username/password, allowing operator injection. The secure version enforces string types and uses explicit operators.

const nano = require('nano')('http://localhost:5984');
const db = nano.db.use('myapp');

// VULNERABLE: Dynamic view creation with user input
app.post('/create-view', async (req, res) => {
    const { viewName, mapFunction } = req.body;
    
    // Dangerous: User-controlled JavaScript code
    const designDoc = {
        _id: '_design/' + viewName,
        views: {
            [viewName]: {
                map: mapFunction  // Direct user input as JavaScript
            }
        }
    };
    
    try {
        await db.insert(designDoc);
        res.json({ message: 'View created successfully' });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// Malicious input:
// mapFunction: "function(doc) { while(true) {} }"  // DoS attack
// mapFunction: "function(doc) { require('fs').writeFileSync('/tmp/pwned', 'hacked'); }"
const nano = require('nano')('http://localhost:5984');
const db = nano.db.use('myapp');

// SECURE: Predefined view templates
const ALLOWED_VIEW_TEMPLATES = {
    'user-by-email': {
        map: 'function(doc) { if (doc.type === "user" && doc.email) { emit(doc.email, doc); } }'
    },
    'posts-by-date': {
        map: 'function(doc) { if (doc.type === "post" && doc.created_at) { emit(doc.created_at, doc); } }'
    },
    'users-by-status': {
        map: 'function(doc) { if (doc.type === "user" && doc.status) { emit(doc.status, doc); } }'
    }
};

function validateViewName(viewName) {
    // Only allow alphanumeric and hyphens
    if (!/^[a-z0-9-]+$/.test(viewName)) {
        throw new Error('Invalid view name format');
    }
    
    if (viewName.length > 50) {
        throw new Error('View name too long');
    }
    
    return viewName;
}

app.post('/create-view', async (req, res) => {
    try {
        const { viewName, templateName } = req.body;
        
        // Validate view name
        const validatedViewName = validateViewName(viewName);
        
        // SECURE: Only allow predefined templates
        if (!ALLOWED_VIEW_TEMPLATES[templateName]) {
            return res.status(400).json({ 
                error: 'Invalid template name',
                allowedTemplates: Object.keys(ALLOWED_VIEW_TEMPLATES)
            });
        }
        
        // Check if view already exists
        try {
            await db.get('_design/' + validatedViewName);
            return res.status(409).json({ error: 'View already exists' });
        } catch (error) {
            // View doesn't exist, which is what we want
        }
        
        const designDoc = {
            _id: '_design/' + validatedViewName,
            views: {
                [validatedViewName]: ALLOWED_VIEW_TEMPLATES[templateName]
            }
        };
        
        await db.insert(designDoc);
        
        res.json({ 
            message: 'View created successfully',
            viewName: validatedViewName,
            template: templateName
        });
        
    } catch (error) {
        console.error('View creation error:', error);
        res.status(500).json({ error: 'View creation failed' });
    }
});

// Secure view querying with parameter validation
app.get('/query-view/:designDoc/:viewName', async (req, res) => {
    try {
        const { designDoc, viewName } = req.params;
        
        // Validate design doc and view names
        validateViewName(designDoc);
        validateViewName(viewName);
        
        // Validate query parameters
        const options = {};
        
        if (req.query.startkey) {
            try {
                options.startkey = JSON.parse(req.query.startkey);
            } catch (e) {
                return res.status(400).json({ error: 'Invalid startkey format' });
            }
        }
        
        if (req.query.endkey) {
            try {
                options.endkey = JSON.parse(req.query.endkey);
            } catch (e) {
                return res.status(400).json({ error: 'Invalid endkey format' });
            }
        }
        
        // Limit results
        options.limit = Math.min(parseInt(req.query.limit) || 50, 1000);
        
        const result = await db.view(designDoc, viewName, options);
        
        res.json({
            rows: result.rows,
            total_rows: result.total_rows,
            offset: result.offset
        });
        
    } catch (error) {
        console.error('View query error:', error);
        res.status(500).json({ error: 'Query failed' });
    }
});

💡 Why This Fix Works

The vulnerable CouchDB code allows arbitrary JavaScript execution through user-controlled map functions. The secure version uses predefined templates and strict validation.

from flask import Flask, request, jsonify
from pymongo import MongoClient
import json

app = Flask(__name__)
client = MongoClient('mongodb://localhost:27017/')
db = client.myapp

@app.route('/users', methods=['GET'])
def get_users():
    # VULNERABLE: Direct JSON parsing from user input
    filter_param = request.args.get('filter', '{}')
    sort_param = request.args.get('sort', '{}')
    
    try:
        # Dangerous: Unvalidated filter from user
        user_filter = json.loads(filter_param)
        user_sort = json.loads(sort_param)
        
        # Can execute arbitrary MongoDB operations
        users = list(db.users.find(user_filter).sort(user_sort))
        return jsonify(users)
    except Exception as e:
        return jsonify({'error': str(e)})

# Malicious URL:
# /users?filter={"$where": "sleep(5000) || true"}&sort={"$natural": -1}
# Causes 5-second delay on database server
from flask import Flask, request, jsonify
from pymongo import MongoClient
import json
import re
from typing import Dict, Any

app = Flask(__name__)
client = MongoClient('mongodb://localhost:27017/')
db = client.myapp

# Whitelist of allowed fields and operators
ALLOWED_FIELDS = ['username', 'email', 'age', 'status', 'created_at']
ALLOWED_OPERATORS = ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin']
ALLOWED_SORT_FIELDS = ['username', 'email', 'age', 'created_at']

def validate_filter(filter_obj: Dict[str, Any]) -> Dict[str, Any]:
    """Validate and sanitize MongoDB filter object"""
    if not isinstance(filter_obj, dict):
        raise ValueError("Filter must be an object")
    
    validated_filter = {}
    
    for field, condition in filter_obj.items():
        # Validate field names
        if field not in ALLOWED_FIELDS:
            raise ValueError(f"Field '{field}' not allowed")
        
        # Handle simple equality
        if isinstance(condition, (str, int, float, bool)):
            if isinstance(condition, str):
                # Sanitize string values
                if len(condition) > 100:
                    raise ValueError("String value too long")
                # Remove potentially dangerous characters
                condition = re.sub(r'[{}()\[\]$]', '', condition)
            validated_filter[field] = condition
        
        # Handle operator objects
        elif isinstance(condition, dict):
            validated_condition = {}
            for operator, value in condition.items():
                if operator not in ALLOWED_OPERATORS:
                    raise ValueError(f"Operator '{operator}' not allowed")
                
                # Validate operator values
                if isinstance(value, str) and len(value) > 100:
                    raise ValueError("Operator value too long")
                
                validated_condition[operator] = value
            validated_filter[field] = validated_condition
        else:
            raise ValueError(f"Invalid condition type for field '{field}'")
    
    return validated_filter

def validate_sort(sort_obj: Dict[str, Any]) -> Dict[str, Any]:
    """Validate and sanitize MongoDB sort object"""
    if not isinstance(sort_obj, dict):
        raise ValueError("Sort must be an object")
    
    validated_sort = {}
    for field, direction in sort_obj.items():
        if field not in ALLOWED_SORT_FIELDS:
            raise ValueError(f"Sort field '{field}' not allowed")
        
        if direction not in [1, -1, "asc", "desc"]:
            raise ValueError(f"Invalid sort direction for field '{field}'")
        
        # Convert string directions to integers
        if direction == "asc":
            direction = 1
        elif direction == "desc":
            direction = -1
        
        validated_sort[field] = direction
    
    return validated_sort

@app.route('/users', methods=['GET'])
def get_users():
    try:
        filter_param = request.args.get('filter', '{}')
        sort_param = request.args.get('sort', '{"created_at": -1}')
        limit = min(int(request.args.get('limit', 50)), 100)  # Max 100 results
        
        # Parse and validate filter
        try:
            raw_filter = json.loads(filter_param)
            user_filter = validate_filter(raw_filter)
        except (json.JSONDecodeError, ValueError) as e:
            return jsonify({'error': f'Invalid filter: {str(e)}'}), 400
        
        # Parse and validate sort
        try:
            raw_sort = json.loads(sort_param)
            user_sort = validate_sort(raw_sort)
        except (json.JSONDecodeError, ValueError) as e:
            return jsonify({'error': f'Invalid sort: {str(e)}'}), 400
        
        # SECURE: Execute validated query with limits
        users = list(db.users.find(
            user_filter,
            {'password': 0, '_id': 0}  # Exclude sensitive fields
        ).sort(list(user_sort.items())).limit(limit))
        
        return jsonify({
            'users': users,
            'count': len(users),
            'limit': limit
        })
        
    except Exception as e:
        # Log error for monitoring
        app.logger.error(f'Database query error: {str(e)}')
        return jsonify({'error': 'Query failed'}), 500

if __name__ == '__main__':
    app.run(debug=False)  # Never run debug in production

💡 Why This Fix Works

The vulnerable Python code directly parses user JSON without validation. The secure version implements comprehensive input validation with whitelists for fields and operators.

Why it happens

MongoDB's $where operator and map-reduce functions can execute JavaScript code, making them vulnerable to code injection attacks. When user input is directly embedded into JavaScript expressions, attackers can execute arbitrary JavaScript code on the database server.

Root causes

JavaScript Code Injection in MongoDB

MongoDB's $where operator and map-reduce functions can execute JavaScript code, making them vulnerable to code injection attacks. When user input is directly embedded into JavaScript expressions, attackers can execute arbitrary JavaScript code on the database server.

Preview example – JAVASCRIPT
// Vulnerable MongoDB query with $where
db.users.find({
    $where: "this.username == '" + userInput + "'"
});
// Attacker input: "'; return true; //"
// Executes: this.username == ''; return true; //'

JSON/Object Injection in Query Parameters

Many NoSQL databases accept JSON objects as query parameters. When applications directly pass user-controlled JSON to database queries without validation, attackers can inject additional query operators or modify query logic.

Preview example – PYTHON
# Vulnerable: Direct JSON parameter passing
# URL: /api/users?filter={"username": "admin", "$where": "sleep(5000)"}
user_filter = json.loads(request.args.get('filter'))
result = db.users.find(user_filter)  # Executes malicious $where

NoSQL Operator Injection

NoSQL databases use special operators (like $gt, $lt, $ne, $regex) that can be injected to modify query behavior. Attackers can bypass authentication, extract data, or perform unauthorized operations by injecting these operators into user input fields.

Preview example – JAVASCRIPT
// Vulnerable: Operator injection in authentication
// POST data: {"username": {"$ne": ""}, "password": {"$ne": ""}}
app.post('/login', (req, res) => {
    db.users.findOne({
        username: req.body.username,  // Can be an object with operators
        password: req.body.password   // Bypasses authentication
    });
});

Unvalidated Aggregation Pipeline Injection

MongoDB's aggregation framework allows complex data processing pipelines. When user input is used to construct aggregation stages without validation, attackers can inject malicious pipeline stages to access unauthorized data or cause denial of service.

Preview example – JAVASCRIPT
// Vulnerable aggregation pipeline construction
function getUserStats(userId, additionalStages) {
    const pipeline = [
        { $match: { userId: userId } },
        ...JSON.parse(additionalStages)  // Dangerous: Unvalidated stages
    ];
    return db.activities.aggregate(pipeline);
}

Fixes

1

Use Parameterized Queries and Avoid Dynamic JavaScript

Avoid using $where operators and JavaScript execution in NoSQL queries. Use native query operators instead of JavaScript expressions. When JavaScript is necessary, use parameterized functions and strict input validation.

View implementation – JAVASCRIPT
// SECURE: Using native MongoDB operators instead of $where
// Instead of: db.users.find({ $where: "this.age > " + userAge })

// Use native operators:
db.users.find({ age: { $gt: parseInt(userAge) } });

// If JavaScript is absolutely necessary:
db.users.find({
    $where: function() {
        return this.age > userAge;  // userAge is passed as parameter
    }
}).hint({ age: 1 });  // Force index usage
2

Implement Strong Type Validation for Query Parameters

Validate that query parameters match expected types and structures. Reject objects when strings are expected, validate operator usage, and implement whitelist validation for allowed query operators and fields.

View implementation – JAVASCRIPT
const allowedOperators = ['$eq', '$gt', '$lt', '$gte', '$lte', '$in'];
const allowedFields = ['username', 'email', 'age', 'status'];

function validateQuery(queryObj) {
    for (let field in queryObj) {
        // Validate field names
        if (!allowedFields.includes(field)) {
            throw new Error(`Invalid field: ${field}`);
        }
        
        // Validate operators
        if (typeof queryObj[field] === 'object' && queryObj[field] !== null) {
            for (let operator in queryObj[field]) {
                if (!allowedOperators.includes(operator)) {
                    throw new Error(`Invalid operator: ${operator}`);
                }
            }
        }
    }
    return queryObj;
}
3

Sanitize and Validate User Input

Implement comprehensive input validation for all user data before using it in NoSQL queries. Use type checking, length limits, format validation, and escape special characters that could be used for injection attacks.

View implementation – PYTHON
import re
from typing import Any, Dict

def sanitize_mongodb_input(user_input: Any, expected_type: str) -> Any:
    """Sanitize user input for MongoDB queries"""
    
    if expected_type == 'string':
        if not isinstance(user_input, str):
            raise ValueError("Expected string input")
        
        # Length validation
        if len(user_input) > 100:
            raise ValueError("Input too long")
        
        # Remove potentially dangerous characters
        sanitized = re.sub(r'[{}()\[\]$]', '', user_input)
        return sanitized
    
    elif expected_type == 'integer':
        try:
            return int(user_input)
        except (ValueError, TypeError):
            raise ValueError("Invalid integer input")
    
    elif expected_type == 'object':
        if not isinstance(user_input, dict):
            raise ValueError("Expected object input")
        
        # Recursively validate object properties
        return validate_query_object(user_input)
    
    raise ValueError(f"Unsupported type: {expected_type}")
4

Use Database-Specific Security Features

Enable database security features like authentication, authorization, role-based access control, and query logging. Configure databases to run with minimal privileges and disable dangerous features like server-side JavaScript execution when not needed.

View implementation – JAVASCRIPT
// MongoDB security configuration
// In mongod.conf:
security:
  authorization: enabled
  javascriptEnabled: false  # Disable server-side JS

setParameter:
  failIndexKeyTooLong: false
  notablescan: true  # Force index usage

// Create limited user roles
db.createRole({
    role: "appUser",
    privileges: [
        {
            resource: { db: "myapp", collection: "users" },
            actions: ["find", "insert", "update"]
        }
    ],
    roles: []
});
5

Implement Query Monitoring and Rate Limiting

Monitor NoSQL queries for suspicious patterns, implement query timeout limits, and use rate limiting to prevent abuse. Log all database operations and implement alerting for potential injection attempts.

View implementation – JAVASCRIPT
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');

// Rate limiting for database operations
const dbRateLimit = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per windowMs
    message: 'Too many database requests'
});

// Slow down repeated requests
const speedLimiter = slowDown({
    windowMs: 15 * 60 * 1000,
    delayAfter: 50,
    delayMs: 500
});

// Query monitoring middleware
function monitorQuery(collection, query) {
    const suspiciousPatterns = [
        /\$where/,
        /\$regex.*\*/,
        /sleep\(/,
        /eval\(/
    ];
    
    const queryString = JSON.stringify(query);
    for (let pattern of suspiciousPatterns) {
        if (pattern.test(queryString)) {
            console.warn(`Suspicious query detected: ${queryString}`);
            throw new Error('Potentially malicious query blocked');
        }
    }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies nosql injection vulnerabilities and many other security issues in your codebase.