Cuộc gọi đánh thức lúc 2:15 sáng: Phân tích sau sự cố Mass Assignment
Cảnh báo PagerDuty của tôi không chỉ kêu bíp bíp; nó gào thét. Vào lúc 2:15 sáng, dashboard Datadog của chúng tôi hiển thị 450 thay đổi ‘role’ trái phép trong vòng chưa đầy ba phút. Vào thời điểm tôi mở laptop, một người dùng dùng thử đã tự nâng cấp mình lên thành ‘superuser’ và bắt đầu đánh cắp 15.000 hồ sơ khách hàng. Cuộc tấn công không sử dụng một lỗi zero-day hào nhoáng hay kỹ thuật SQL injection phức tạp nào. Đó là một lỗ hổng Mass Assignment đơn giản ẩn trong một endpoint cập nhật hồ sơ.
Hãy coi Mass Assignment như một vấn đề “chia sẻ quá mức”. Nó xảy ra khi một ứng dụng lấy dữ liệu đầu vào thô của người dùng và gán trực tiếp nó vào mô hình cơ sở dữ liệu nội bộ mà không lọc. Nếu logic backend của bạn trông giống như User.update(req.body), bạn đã thực sự cấp quyền ghi vào toàn bộ schema cho mọi người dùng API. Kẻ tấn công chỉ cần thêm "is_admin": true vào payload JSON của chúng, và cơ sở dữ liệu của bạn sẽ vui vẻ chấp nhận.
IDOR (Insecure Direct Object Reference) là người anh em nguy hiểm không kém. Điều này xảy ra khi một ứng dụng tin tưởng vào đầu vào của người dùng để truy xuất các bản ghi cụ thể. Nếu tôi có thể thay đổi /api/orders/1024 thành /api/orders/1025 và xem hóa đơn riêng tư của bạn, thì logic phân quyền của bạn đã bị hỏng. Cả hai lỗ hổng này đều đại diện cho sự thất bại trong Kiểm soát truy cập cấp đối tượng (OLAC) — quy trình xác minh không chỉ người dùng là ai, mà còn là họ có thể chạm vào dữ liệu cụ thể nào.
Phân tích bề mặt tấn công
Trong sự cố mà tôi quản lý, nhà phát triển đã sử dụng một ORM hiện đại để đẩy nhanh tiến độ MVP. Logic có vẻ hợp lý: “Người dùng đã đăng nhập, vì vậy họ có thể cập nhật hồ sơ của chính mình.” Tuy nhiên, điều này đã bỏ qua khoảng cách giữa Xác thực (Authentication – chứng minh danh tính) và Phân quyền (Authorization – xác minh quyền hạn đối với một tài nguyên cụ thể).
Bảo mật bắt đầu từ những điều cơ bản, ngay cả trong cơn khủng hoảng. Khi thiết lập môi trường staging cho bản sửa lỗi này, tôi đã tạo các server secret mới bằng công cụ tại toolcraft.app/vi/tools/security/password-generator. Vì nó chạy hoàn toàn trong trình duyệt, không có chuỗi nhạy cảm nào được gửi qua mạng. Đó là một thói quen nhỏ, sạch sẽ và mang lại hiệu quả khi áp lực dâng cao.
Phòng thí nghiệm: Xây dựng một API có lỗ hổng
Để bịt một lỗ rò rỉ, bạn phải thấy nước chảy ra từ đâu. Dưới đây là một môi trường Node.js rút gọn phản ánh các lỗi chúng tôi đã tìm thấy trong sản phẩm thực tế. Bạn có thể chạy mã này trên bất kỳ máy nào đã cài đặt Node.js.
# Thiết lập môi trường
mkdir security-lab && cd security-lab
npm init -y
npm install express
Tạo tệp app.js với mã bên dưới. Điều này đại diện cho cách tiếp cận điển hình “làm nhanh và chấp nhận sai sót” dẫn đến nợ bảo mật:
const express = require('express');
const app = express();
app.use(express.json());
// Cơ sở dữ liệu giả lập
let users = {
"1": { id: 1, username: "alice", role: "user", bio: "Xin chào!" },
"2": { id: 2, username: "bob", role: "user", bio: "Tôi yêu bảo mật." }
};
// LỖ HỔNG: Mass Assignment
app.put('/api/profile', (req, res) => {
const userId = "1"; // Giả lập người dùng trong session
// Lỗi: gán req.body trực tiếp vào đối tượng cơ sở dữ liệu mà không kiểm soát
Object.assign(users[userId], req.body);
res.send({ message: "Đã cập nhật hồ sơ", user: users[userId] });
});
// LỖ HỔNG: IDOR
app.get('/api/orders/:id', (req, res) => {
const orderId = req.params.id;
// Lỗi: Không kiểm tra xem orderId có thuộc về người yêu cầu hay không
res.send({ orderId: orderId, data: "Dữ liệu hóa đơn nhạy cảm cho ID " + orderId });
});
app.listen(3000, () => console.log('Máy chủ có lỗ hổng đang chạy tại cổng 3000'));
Giải pháp: Triển khai kiểm soát truy cập cấp đối tượng
Để lấp những lỗ hổng này, chúng ta cần ngừng ưu tiên sự tiện lợi của nhà phát triển hơn tính toàn vẹn của dữ liệu. Chúng ta cần hai thứ: danh sách cho phép (allow-listing) nghiêm ngặt cho các bản cập nhật và xác minh quyền sở hữu để truy cập dữ liệu.
Bảo mật cập nhật với Đối tượng chuyển đổi dữ liệu (DTO)
Đừng bao giờ để req.body chạm trực tiếp vào cơ sở dữ liệu của bạn. Thay vào đó, hãy trích xuất rõ ràng các trường bạn muốn cho phép. Thay đổi đơn giản này khiến kẻ tấn công không thể chèn trường role hoặc balance vào truy vấn cập nhật.
// BẢO MẬT: Sử dụng danh sách cho phép thủ công
app.put('/api/profile/secure', (req, res) => {
const userId = "1";
const { bio, location } = req.body; // Chỉ trích xuất những trường an toàn
if (bio) users[userId].bio = bio;
if (location) users[userId].location = location;
res.send({ message: "Cập nhật thành công", user: users[userId] });
});
Ngăn chặn IDOR bằng Middleware xác minh quyền sở hữu
Để đánh bại IDOR, mọi yêu cầu tài nguyên phải được kiểm soát. Bạn phải xác minh rằng ID người dùng đã xác thực khớp với owner_id của tài nguyên được yêu cầu trong cơ sở dữ liệu của bạn.
// BẢO MẬT: Xác minh quyền sở hữu
const verifyOwnership = (req, res, next) => {
const currentUserId = 1; // Lấy từ JWT hoặc Session của bạn
const requestedOrderId = req.params.id;
// Thực tế: const order = await db.orders.findById(requestedOrderId);
const orderOwnerId = 2; // Giả sử đơn hàng 101 thuộc về Bob (ID 2)
if (currentUserId !== orderOwnerId) {
return res.status(403).json({ error: "Truy cập bị từ chối: Bạn không sở hữu tài nguyên này" });
}
next();
};
app.get('/api/orders/secure/:id', verifyOwnership, (req, res) => {
res.send({ orderId: req.params.id, data: "Dữ liệu hóa đơn bảo mật" });
});
Xác thực: Kiểm tra các lớp phòng thủ bằng Postman
Khi các bản sửa lỗi của bạn đã hoạt động, bạn cần chứng minh chúng hiệu quả. Các bài kiểm tra bảo mật tự động trong Postman là cách tốt nhất để ngăn chặn các lỗ hổng này quay trở lại codebase của bạn trong các sprint tương lai.
Xác minh lớp bảo vệ Mass Assignment
- Gửi yêu cầu đến endpoint
PUTtạihttp://localhost:3000/api/profile/secure. - Đặt body thành
JSON. - Thử tấn công:
{ "bio": "Hacked", "role": "admin" }. - Gửi yêu cầu.
Phản hồi sẽ hiển thị bio đã thay đổi, nhưng role vẫn phải là user. API đã bỏ qua thành công trường không được phép.
Xác minh lớp bảo vệ IDOR
- Gửi yêu cầu
GETđếnhttp://localhost:3000/api/orders/secure/101. - Vì middleware của chúng ta mô phỏng sự không khớp (Alice cố gắng xem dữ liệu của Bob), máy chủ phải trả về lỗi
403 Forbidden.
Theo dõi các dấu hiệu cảnh báo
Phát hiện cũng quan trọng như ngăn chặn. Bạn cần biết khi nào ai đó đang dò tìm vành đai của bạn. Tôi khuyên bạn nên theo dõi hai chỉ số cụ thể:
- Sự gia tăng đột biến lỗi 403 Forbidden: Nếu một IP duy nhất truy cập vào 20 ID tài nguyên khác nhau và gây ra lỗi 403 trong vòng 60 giây, rất có thể họ đang chạy một script liệt kê IDOR.
- Vi phạm Schema: Sử dụng một thư viện như Zod hoặc Joi. Ghi nhật ký mọi trường hợp người dùng gửi một trường không có trong schema của bạn. Việc thấy
permissionshoặcis_admintrong log là một dấu hiệu rõ rệt của một cuộc tấn công đang diễn ra.
Tôi thường chuyển các sự kiện này vào cảnh báo Slack. Nếu vượt quá ngưỡng, WAF của chúng tôi sẽ tự động ngắt kết nối. Đó là cách duy nhất để đảm bảo cuộc gọi pager lúc 2:15 sáng đó không bao giờ xảy ra nữa.

