Skip to content

MongoDB Security

Unsecured MongoDB instances are the most scanned database on the internet Shodan sees thousands of open MongoDB ports every day and automated ransomware scripts specifically target instances without authentication because the MongoDB protocol makes it trivial to enumerate databases, extract documents, and drop collections in a single query - and once your data is encrypted with a ransom note demanding Bitcoin, you're not getting it back unless you have backups

Authentication - lock the door

MongoDB supports multiple authentication mechanisms from simple password auth to x.509 certificates and LDAP integration. Enable authentication before you connect to anything beyond localhost

// mongod.conf - enable authentication
// security:
//   authorization: "enabled"

// Create admin user first
use admin
db.createUser({
    user: "admin",
    pwd: "very_secure_password_123",
    roles: [
        { role: "root", db: "admin" }
    ]
});

// Create application user with least privilege
use myapp
db.createUser({
    user: "api_service",
    pwd: "app_specific_password",
    roles: [
        { role: "readWrite", db: "myapp" },
        { role: "read", db: "logs" }
    ]
});

// Create read-only user for analytics
db.createUser({
    user: "analytics",
    pwd: "readonly_password",
    roles: [
        { role: "read", db: "myapp" }
    ]
});

Authorization - roles and privileges

MongoDB's role-based access control uses built-in roles (read, readWrite, dbAdmin, userAdmin, clusterAdmin) and custom roles for fine-grained control. Never use the root role for application connections - that's like giving the valet the master key to your house

// Built-in roles
// read: read data in specific database
// readWrite: read and write data
// dbAdmin: manage database schema and indexes
// userAdmin: manage users for the database
// clusterAdmin: manage cluster (sharding, replication)
// root: superuser access to everything

// Create a custom role with specific privileges
db.createRole({
    role: "orderManager",
    privileges: [
        {
            resource: { db: "myapp", collection: "orders" },
            actions: ["find", "insert", "update", "remove"]
        },
        {
            resource: { db: "myapp", collection: "users" },
            actions: ["find"]  // Read user data but can't modify
        }
    ],
    roles: []  // Inherit from other roles
});

// Create user with custom role
db.createUser({
    user: "order_service",
    pwd: "order_pwd",
    roles: ["orderManager"]
});

Network security - who can talk to MongoDB

The default MongoDB configuration binds to 0.0.0.0 (all interfaces) which means any machine that can reach your server can attempt to connect. Change this immediately

# mongod.conf - bind to specific interfaces
net:
  bindIp: 127.0.0.1,10.0.1.50  # localhost + internal app server IP
  port: 27017
  # Optionally use a non-default port (security through obscurity , not a real defense)
  # port: 37017

# With iptables - restrict access to specific source IPs
iptables -A INPUT -p tcp --dport 27017 -s 10.0.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 27017 -j DROP

# With UFW
sudo ufw allow from 10.0.1.0/24 to any port 27017
sudo ufw deny 27017

Encryption at rest

MongoDB Enterprise supports encryption at rest using WiredTiger's native encryption engine with AES-256-CBC (or AES-256-GCM in newer versions). The encryption key is stored externally and MongoDB decrypts data in memory when reading

# mongod.conf - WiredTiger encryption at rest
storage:
  dbPath: /var/lib/mongodb
  wiredTiger:
    engineConfig:
      encryptWith: "AES256-CBC"
      encryptionKeyManager:
        keyFile: /etc/mongodb/encryption-key-file
        rotateMasterKey: false

For the community edition, use filesystem-level encryption (LUKS, dm-crypt) or cloud-provider disk encryption (EBS encryption, GCE PD encryption)

NoSQL injection - MongoDB edition

NoSQL injection attacks MongoDB by injecting query operators through unsanitized input. Unlike SQL injection which exploits string concatenation to break out of quotes, NoSQL injection exploits MongoDB's query language itself - when the application passes user input directly into a query filter without type checking, the attacker can send an object with $gt, $ne, $regex, or $where operators to manipulate the query

// VULNERABLE - passes user input directly into query
app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    // This is a MongoDB query object constructed from user input
    const user = await db.collection('users').findOne({
        username: username,
        password: password
    });
    if (user) {
        res.json({ token: createToken(user) });
    } else {
        res.status(401).json({ error: 'Invalid credentials' });
    }
});

// Attacker sends JSON body:
// {
//     "username": { "$gt": "" },
//     "password": { "$gt": "" }
// }
// Query becomes: db.users.findOne({ username: { $gt: "" }, password: { $gt: "" } })
// $gt: "" matches any string greater than empty string - which is EVERY string
// Returns the FIRST user in the collection
// Authentication bypassed. Welcome , attacker. Enjoy your admin access.

SQL vs NoSQL injection comparison

SQL injection:    ' OR '1'='1
NoSQL injection:  { "$gt": "" }

SQL UNION:        ' UNION SELECT * FROM users --
NoSQL operator:   { "$regex": ".*" }

SQL blind:        ' AND (SELECT COUNT(*) FROM users) > 100 --
NoSQL blind:      { "$where": "this.password.length > 30" }

The $where operator is particularly dangerous because it executes JavaScript within the database. If an attacker can inject into a $where clause, they can execute arbitrary JavaScript on the server

// VULNERABLE - $where with user input
db.users.find({ $where: `this.username === '${userInput}'` });

// Attacker sends: ' || sleep(5000) || '
// Waits 5 seconds - confirms injection works

// Attacker sends: '; while(true) {};'
// DoS - infinite loop on the database server

Mitigation

// SECURE - always validate input types before querying
app.post('/login', async (req, res) => {
    const { username, password } = req.body;

    // Reject anything that isn't a primitive string
    if (typeof username !== 'string' || typeof password !== 'string') {
        return res.status(400).json({ error: 'Invalid input format' });
    }

    // Sanitize against operators in the value itself
    const sanitized = username.replace(/[{}()$]/, '');
    if (sanitized !== username) {
        return res.status(400).json({ error: 'Invalid characters in input' });
    }

    const user = await db.collection('users').findOne({
        username: username,
        password: password  // But wait - password is probably hashed
    });
});

// Better approach - use a library that sanitizes queries
// const mongoSanitize = require('express-mongo-sanitize');
// app.use(mongoSanitize());  // Strips $ and . from req.body, req.query, req.params

Using Mongoose (safe by default with schema validation)

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
    email: { type: String, required: true },
    username: { type: String, required: true, minlength: 3 },
    role: { type: String, enum: ['user', 'admin'] },
    passwordHash: { type: String, required: true }
});

// Mongoose validates types - if req.body.username is an object like { $gt: "" }
// Mongoose will reject it because the schema says String, not Object
const user = await User.findOne({ email: req.body.email });

// But be careful - Mongoose doesn't protect against all injection vectors
// With .where() and other chainable methods, injection is still possible
// Always prefer findByEmail() style methods over raw .where() chains

Auditing

MongoDB Enterprise includes audit logging that records all database operations for compliance and forensic investigation

# mongod.conf - enable audit
auditLog:
  destination: file
  format: JSON
  path: /var/log/mongodb/audit.log
  filter: '{ atype: { $in: ["authenticate", "createUser", "dropCollection", "dropDatabase", "createIndex", "dropIndex"] } }'

The audit log captures who did what, when, from where, and what data was affected - essential for breach investigation and compliance frameworks like SOC2 and PCI-DSS

prerequisites

db_08_mongo_indexes.md - understanding security requires understanding how queries execute. NoSQL injection works by manipulating query operators, and you need to know how those operators work to see how injection pens them to bypass authentication


next → db_10_redis_intro.md