NoSQL Databases: MongoDB & Redis

Explore document (MongoDB) and key-value (Redis) NoSQL databases — their data models, query languages, use cases, and security considerations.

Medium 65m 3 tasks

Learning Objectives

  • Understand NoSQL trade-offs vs relational databases
  • Perform CRUD operations in MongoDB using the shell and pymongo
  • Query documents with filters, projection, and aggregation pipelines
  • Use Redis for caching, sessions, and rate limiting
  • Identify NoSQL injection and misconfiguration vulnerabilities

SQL vs NoSQL Trade-offs

SQL (Relational) NoSQL
Schema Fixed, predefined Flexible, dynamic
Relations JOINs Embedded docs / denormalized
Scaling Vertical (bigger server) Horizontal (more servers)
ACID Full Varies (BASE model)
Query SQL standard Database-specific
Best for Complex queries, integrity Scale, flexible schema, speed

CAP Theorem — distributed systems can only guarantee 2 of 3:
- Consistency — every read gets the latest write
- Availability — every request gets a response
- Partition tolerance — system works despite network splits

MongoDB — Document Store

MongoDB stores JSON-like documents in collections (like tables):

{
  "_id": "ObjectId('64a1f...')",
  "host": "10.0.0.1",
  "status": "complete",
  "created_at": "2025-01-15T10:30:00Z",
  "ports": [
    { "port": 22,  "service": "ssh",   "status": "open" },
    { "port": 80,  "service": "http",  "status": "open" },
    { "port": 443, "service": "https", "status": "closed" }
  ],
  "user": { "id": 1, "username": "alice" }
}

MongoDB Shell (mongosh)

use cyberlearn              // switch database
show collections            // list collections
db.scans.find()            // all documents
db.scans.findOne()         // first document
db.scans.countDocuments()  // count

CRUD Operations

// INSERT
db.scans.insertOne({
  host: "10.0.0.1",
  status: "pending",
  user_id: 1,
  created_at: new Date()
});

db.scans.insertMany([
  { host: "10.0.0.2", status: "complete" },
  { host: "10.0.0.3", status: "pending"  },
]);

// READ — filter, projection, options
db.scans.find(
  { status: "complete", user_id: 1 },   // filter
  { host: 1, created_at: 1, _id: 0 }   // projection (1=include, 0=exclude)
);

// Comparison operators
db.scans.find({ port_count: { $gt: 5 } });
db.scans.find({ status: { $in: ["complete", "partial"] } });
db.scans.find({ "ports.service": "ssh" });  // nested field

// Logical
db.scans.find({ $and: [{ status: "open" }, { port: { $lt: 1024 } }] });
db.scans.find({ $or:  [{ status: "open" }, { status: "filtered" }] });

// Sort, limit, skip
db.scans.find().sort({ created_at: -1 }).limit(10).skip(20);

// UPDATE
db.scans.updateOne(
  { _id: ObjectId("...") },
  { $set: { status: "complete", ended_at: new Date() } }
);

db.scans.updateMany(
  { status: "pending", created_at: { $lt: new Date("2024-01-01") } },
  { $set: { status: "expired" } }
);

// DELETE
db.scans.deleteOne({ _id: ObjectId("...") });
db.scans.deleteMany({ status: "expired" });

Aggregation Pipeline

db.scans.aggregate([
  // Stage 1: filter
  { $match: { status: "complete" } },

  // Stage 2: unwind array into separate docs
  { $unwind: "$ports" },

  // Stage 3: filter unwound docs
  { $match: { "ports.status": "open" } },

  // Stage 4: group and count
  { $group: {
    _id: "$ports.service",
    count: { $sum: 1 },
    hosts: { $addToSet: "$host" }
  }},

  // Stage 5: sort
  { $sort: { count: -1 } },

  // Stage 6: limit
  { $limit: 10 }
]);

pymongo (Python)

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017/")
db     = client["cyberlearn"]
scans  = db["scans"]

# Insert
result = scans.insert_one({"host": "10.0.0.1", "status": "pending"})
scan_id = result.inserted_id

# Find
for scan in scans.find({"status": "complete"}, {"host": 1, "_id": 0}):
    print(scan["host"])

# Update
scans.update_one({"_id": scan_id}, {"$set": {"status": "complete"}})

# Count
total = scans.count_documents({"status": "complete"})

Redis — Key-Value Store

Redis stores data as key-value pairs in memory — extremely fast (μs latency).

redis-cli              # connect
PING                   # → PONG
KEYS *                 # list all keys (avoid in prod)
DBSIZE                 # number of keys
FLUSHDB                # delete all keys (DANGEROUS)

Redis Data Types

# String
SET   session:abc123 '{"user_id":1}' EX 3600   # set with 1hr TTL
GET   session:abc123
DEL   session:abc123
TTL   session:abc123   # seconds remaining

# List (queue/stack)
RPUSH scan_queue "10.0.0.1"   # push right
LPOP  scan_queue              # pop left (FIFO queue)
LLEN  scan_queue

# Hash (object)
HSET  user:1 username alice email [email protected]
HGET  user:1 username
HGETALL user:1

# Set (unique values)
SADD  scan:hosts "10.0.0.1" "10.0.0.2"
SMEMBERS scan:hosts
SISMEMBER scan:hosts "10.0.0.1"

# Sorted Set (leaderboard, rate limit)
ZADD  leaderboard 1500 "alice" 1200 "bob"
ZRANK leaderboard "alice"
ZREVRANGE leaderboard 0 9 WITHSCORES   # top 10

Redis for Rate Limiting

import redis

r = redis.Redis(host='localhost', port=6379)

def is_rate_limited(user_id: int, limit: int = 10, window: int = 60) -> bool:
    key = f"rate:{user_id}"
    pipe = r.pipeline()
    pipe.incr(key)
    pipe.expire(key, window)
    count, _ = pipe.execute()
    return count > limit

Redis for Session Storage

import json, secrets

def create_session(user_id: int) -> str:
    token = secrets.token_urlsafe(32)
    r.setex(f"session:{token}", 3600, json.dumps({"user_id": user_id}))
    return token

def get_session(token: str) -> dict | None:
    data = r.get(f"session:{token}")
    return json.loads(data) if data else None

NoSQL Security

MongoDB NoSQL Injection

// Vulnerable Express route — user controls filter directly
app.post("/login", (req, res) => {
  const { username, password } = req.body;
  // BUG: attacker sends { "username": { "$ne": "" }, "password": { "$ne": "" } }
  db.users.findOne({ username, password }, callback);
});

// Attacker payload (JSON body):
// { "username": { "$ne": "" }, "password": { "$ne": "" } }
// This matches ANY user where username != "" and password != ""
// → authentication bypass!

// FIX: sanitize inputs — remove $ keys
function sanitize(obj) {
  for (const key of Object.keys(obj)) {
    if (key.startsWith("$")) delete obj[key];
    else if (typeof obj[key] === "object") sanitize(obj[key]);
  }
  return obj;
}

Redis Security Misconfigurations

  • No authenticationredis-cli -h target → full access
  • Bind to 0.0.0.0 — exposed to internet
  • FLUSHDB available — wipe all data
  • CONFIG SET allows writing files → RCE via cron/SSH key injection

Fix:

# redis.conf
bind 127.0.0.1        # only local connections
requirepass your_secret_password
rename-command FLUSHDB ""    # disable dangerous commands
rename-command CONFIG  ""

Using pymongo (or mongosh): (1) Create a 'scans' collection with 5 documents each having host, ports (array), status, created_at. (2) Query: find all complete scans, (3) find scans with SSH (port 22) open using dot notation, (4) update all pending scans older than 1 day to 'expired', (5) delete expired scans.

✦ Answer the questions to complete this task

How do you query a nested field in MongoDB?

What does $set do in an updateOne?

Implement a Redis-based rate limiter: INCR a key per user, set TTL on first increment, return 429 if count exceeds limit. Test with 15 rapid requests on a limit of 10/minute.

✦ Answer the questions to complete this task

What Redis command sets a value with an expiry in seconds?

Why use a Redis pipeline for rate limiting?

Set up a vulnerable MongoDB login endpoint. Send the injection payload: {username: {$ne: ''}, password: {$ne: ''}}. Observe authentication bypass. Implement the sanitize() function to strip $ keys and verify the bypass no longer works.

✦ Answer the questions to complete this task

What MongoDB operator means 'not equal'?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

NoSQL MCQ

NoSQL MCQ

Start →
⚙️ Practical Medium +30 XP

Hybrid Scan Storage

Hybrid Scan Storage

Start →
🚩 Challenge Hard +50 XP

Redis CONFIG RCE

Redis CONFIG RCE

Start →