Bảo Mật REST API: Đừng Để Hệ Thống “Sập” Vì Những Lỗi Cơ Bản

Security tutorial - IT technology blog
Security tutorial - IT technology blog

API của bạn có đang “mở toang” cho hacker?

Nhiều bạn mới làm web thường chỉ ưu tiên việc API chạy thông, trả về đúng JSON là thấy ổn. Nhưng tin mình đi, đưa một API thiếu bảo mật lên internet chẳng khác nào treo biển “mời vào xơi” cho hacker. Thực tế, mình từng thức trắng đêm xử lý một vụ brute-force SSH vào server chỉ vì hở một endpoint test — từ đó mình rút ra bài học: Bảo mật phải được setup ngay từ dòng code đầu tiên, dù dự án lớn hay nhỏ.

API không chỉ là nơi truyền nhận dữ liệu. Nó là cửa ngõ duy nhất để thế giới bên ngoài chạm vào database của bạn. Theo báo cáo của Akamai, tấn công vào API chiếm tới 75% tổng số các cuộc tấn công web hiện nay. Nếu không có lớp phòng thủ, dữ liệu người dùng sẽ bị đánh cắp, thậm chí toàn bộ hệ thống có thể bị xóa sạch trong tích tắc.

3 lớp phòng thủ “bất di bất dịch”

Để bảo vệ API toàn diện, mình luôn áp dụng quy tắc 3 lớp mà bất kỳ Senior nào cũng yêu cầu:

  • Authentication (Xác thực): Xác minh danh tính. Bạn phải chứng minh được mình là ai trước khi vào hệ thống.
  • Authorization (Phân quyền): Kiểm soát hành vi. Một khách hàng không thể có quyền xóa sản phẩm của chủ shop.
  • Data Validation (Kiểm soát dữ liệu): Lọc mã độc. Bước này giúp chặn đứng các kỹ thuật như SQL Injection hay XSS ngay từ vòng gửi xe.

1. Xác thực thông minh với JSON Web Token (JWT)

Session truyền thống thường gây khó khăn khi scale (mở rộng) server. JWT giải quyết việc này bằng cơ chế stateless (không lưu trạng thái trên server). Khi đăng nhập thành công, server trả về một token. Ở các request sau, client chỉ cần đính kèm token này vào header là xong.

Đây là cách mình triển khai middleware kiểm tra JWT trong Node.js để đảm bảo an toàn:

const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) return res.status(401).json({ message: "Bạn cần đăng nhập để thực hiện thao tác này!" });

    jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
        if (err) return res.status(403).json({ message: "Token đã hết hạn hoặc không hợp lệ!" });
        req.user = user;
        next();
    });
};

Mẹo nhỏ: Hãy đặt thời gian hết hạn (exp) cho token ngắn thôi, khoảng 15-30 phút, và dùng Refresh Token để tăng tính bảo mật.

2. Phân quyền (Authorization) – Chặn đứng lỗi IDOR

Xác thực xong mới chỉ là một nửa chặng đường. Một lỗi cực kỳ phổ biến là IDOR (Insecure Direct Object Reference). Lỗi này xảy ra khi user A chỉ cần thay đổi id trên URL là xem được đơn hàng của user B.

Giải pháp tốt nhất là dùng RBAC (Role-Based Access Control). Bạn cần một middleware kiểm tra vai trò người dùng trước khi cho phép truy cập vào controller.

const authorizeRoles = (...allowedRoles) => {
    return (req, res, next) => {
        if (!allowedRoles.includes(req.user.role)) {
            return res.status(403).json({ message: "Bạn không có quyền truy cập vào khu vực này!" });
        }
        next();
    };
};

// Chỉ Admin mới có quyền xóa sản phẩm
router.delete('/product/:id', authenticateToken, authorizeRoles('admin'), deleteProduct);

3. Validation dữ liệu – Tuyệt đối không tin Client

Hãy luôn mặc định rằng mọi dữ liệu từ Client gửi lên đều là “bẩn”. Chỉ cần một phút lơ là không check kiểu dữ liệu, hacker có thể gửi một đoạn script phá hoại thay vì một con số.

Thay vì viết if/else thủ công, hãy dùng các thư viện như Joi hoặc Zod để định nghĩa schema chặt chẽ:

const Joi = require('joi');

const schema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    email: Joi.string().email().required(),
});

// Kiểm tra dữ liệu đầu vào
const { error } = schema.validate(req.body);
if (error) return res.status(400).send(error.details[0].message);

Những “chốt chặn” thực chiến khác

Ngoài 3 trụ cột trên, mình thường bổ sung thêm các lớp bảo vệ sau để API thực sự lì đòn trước các đợt tấn công tự động:

Bắt buộc sử dụng HTTPS

Thiếu HTTPS, mọi lớp bảo mật phía sau gần như vô nghĩa. Hacker có thể dùng kỹ thuật Man-in-the-middle để “nghe lén” và lấy trộm token dễ dàng. Hãy sử dụng TLS 1.2 hoặc 1.3 để mã hóa toàn bộ đường truyền dữ liệu.

Rate Limiting – Chặn đứng Bot và Spam

Nếu một IP gửi hàng trăm request mỗi giây, đó chắc chắn là bot đang cố gắng phá hoại hoặc brute-force mật khẩu. Thư viện express-rate-limit là cứu cánh giúp bạn giới hạn số lượng yêu cầu trong một khoảng thời gian.

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
    windowMs: 1 * 60 * 1000, // 1 phút
    max: 60, // Tối đa 60 request mỗi phút từ một IP
    message: "Bạn thao tác quá nhanh, vui lòng đợi một chút!"
});

app.use('/api/', limiter);

Dùng Helmet để ẩn thông tin công nghệ

Mặc định, Express thường đính kèm header X-Powered-By: Express. Điều này vô tình chỉ đường cho hacker tìm lỗ hổng của phiên bản framework bạn đang dùng. Chỉ cần cài helmet, thư viện này sẽ tự động dọn dẹp các header nhạy cảm và cấu hình bảo mật cơ bản cho bạn.

Lời kết

Bảo mật API không phải là một đích đến mà là một hành trình liên tục. Đừng đợi đến khi server bị tấn công lúc nửa đêm mới bắt đầu cuống cuồng đi vá lỗi. Hãy rèn luyện thói quen viết code an toàn, rà soát log thường xuyên và cập nhật các bản vá bảo mật cho thư viện ngay khi có thể. Chúc các bạn xây dựng được những hệ thống vững chắc!

Share: