NoSQL Databases: MongoDB & Redis
Explore document (MongoDB) and key-value (Redis) NoSQL databases — their data models, query languages, use cases, and security considerations.
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 authentication —
redis-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.
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.
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.
What MongoDB operator means 'not equal'?