Chuyển đến nội dung chính

Ngăn chặn tấn công SQL/NoSQL Injection trong MongoDB

Bảo vệ MongoDB khỏi NoSQL injection bằng cách kiểm tra đầu vào, bảo mật truy vấn và áp dụng phòng thủ nhiều lớp. Tìm hiểu mẫu tấn công và cách phòng tránh trong hướng dẫn này.
· 6 phút đọc

Khám phá cùng AI

Ứng dụng MongoDB đối mặt với rủi ro bảo mật nghiêm trọng khi đầu vào của người dùng không được kiểm tra. Các cuộc tấn công NoSQL injection có thể âm thầm xâm nhập cơ sở dữ liệu, làm lộ dữ liệu nhạy cảm và ảnh hưởng đến tính toàn vẹn hệ thống. Hướng dẫn này trình bày các chiến lược phòng thủ đã được kiểm chứng để làm sạch đầu vào, bảo mật truy vấn cơ sở dữ liệu và xây dựng lớp bảo vệ vững chắc trước các mối đe doạ dựa trên injection.

NoSQL Injection là gì?

NoSQL injection xảy ra khi kẻ tấn công lợi dụng đầu vào của người dùng không được kiểm tra đầy đủ để thao túng logic truy vấn cơ sở dữ liệu. Bằng cách chèn payload độc hại, kẻ tấn công có thể vượt qua cơ chế xác thực, truy xuất dữ liệu trái phép hoặc lách các kiểm soát bảo mật của ứng dụng.

Không giống SQL injection, khai thác lỗ hổng từ việc ghép chuỗi để tạo truy vấn, NoSQL injection lợi dụng cấu trúc dữ liệu linh hoạt và truy vấn động. Điều này đặc biệt nguy hiểm khi ứng dụng chấp nhận kiểu dữ liệu lỏng như interface{} hoặc map[string]interface{} mà không có kiểm tra phù hợp.

Vì sao MongoDB khác biệt

Truy vấn MongoDB được xây dựng bằng các đối tượng BSON (Binary JSON)—các cặp khoá-giá trị có cấu trúc—thay vì chuỗi thô. Thiết kế này tránh được nhiều rủi ro injection truyền thống vì truy vấn không được lắp ghép qua nối chuỗi.

Tuy nhiên, nếu đầu vào của người dùng được nhúng trực tiếp vào các đối tượng BSON này mà không kiểm tra phù hợp, kẻ tấn công có thể chèn các toán tử MongoDB như $ne, $gt hoặc $or để thay đổi logic truy vấn.

Ví dụ: chèn toán tử trong Go

filter := bson.M{"name": inputName}
collection.Find(ctx, filter)

Nếu inputName là một chuỗi đơn giản như "Joe" thì an toàn. Nhưng nếu mã của bạn cho phép map[string]interface{} hoặc JSON thô, kẻ tấn công có thể chèn các đối tượng toán tử như:

{ "$ne": null }

Payload này biến đổi logic truy vấn, khớp với mọi tài liệu nơi tên tồn tại thay vì lọc theo một giá trị cụ thể—về thực chất là lách các kiểm soát truy cập dự kiến của ứng dụng.

Nhận diện mẫu không an toàn và các bẫy thường gặp

1. Mặc định tin tưởng nội dung request

Parse trực tiếp thân request vào các map BSON tạo ra đường dẫn injection, nơi kẻ tấn công có thể nhúng các toán tử truy vấn độc hại và cấu trúc logic phức tạp trực tiếp vào thao tác với cơ sở dữ liệu.

// Unsafe
var filter bson.M
_ = json.NewDecoder(req.Body).Decode(&filter)

Thay vào đó, chỉ parse các trường và kiểu đã biết:

type QueryInput struct {
    Name string json:"name" validate:"required"
}

2. Chấp nhận kiểu dữ liệu lỏng

Lỗ hổng injection thường xuất hiện khi API chấp nhận các kiểu dữ liệu quá “rộng” như interface{} hoặc map tổng quát. Các cấu trúc linh hoạt này có thể chứa các toán tử MongoDB nhúng hoặc đối tượng độc hại để lách logic truy vấn dự kiến.

Giảm thiểu việc dùng kiểu lỏng nếu không thật sự cần thiết và áp dụng kiểm tra nghiêm ngặt khi không tránh khỏi. Thay vào đó, định nghĩa các cấu trúc dữ liệu tường minh khớp với định dạng đầu vào dự kiến và ràng buộc giá trị chấp nhận được.

3. JSON trong chuỗi truy vấn hoặc toán tử truy vấn

API cung cấp khả năng lọc động qua tham số URL hoặc endpoint tìm kiếm là bề mặt tấn công hấp dẫn. Kẻ tấn công thường nhắm vào các giao diện này bằng cách nhúng toán tử được mã hoá JSON và logic điều kiện trực tiếp vào chuỗi truy vấn. Điều này biến các tham số lọc hợp lệ thành thao tác cơ sở dữ liệu độc hại:

GET /search?filter={"price":{"$gt":0}}

Nếu không có kiểm tra đầu vào nghiêm ngặt, cách làm này cho phép kẻ tấn công thao túng cấu trúc truy vấn và lách các kiểm soát bảo mật, có thể làm lộ dữ liệu trái phép hoặc leo thang đặc quyền vượt ngoài phạm vi truy cập dự kiến.

Giảm thiểu:

  • Triển khai danh sách cho phép cho trường và toán tử để giới hạn tham số truy vấn vào các giá trị an toàn, được định nghĩa trước.
  • Bao bọc rõ ràng mọi giá trị do người dùng cung cấp bằng toán tử $eq khi xây dựng bộ lọc cơ sở dữ liệu để ngăn chèn các toán tử khác.

4. Mở rộng truy vấn logic bằng injection

Ứng dụng cho phép người dùng điều khiển các toán tử logic như $or tạo cơ hội cho tấn công mở rộng truy vấn. Kẻ tấn công có thể chèn thêm biểu thức điều kiện vào các toán tử này, làm thay đổi căn bản ngữ nghĩa truy vấn và có thể lách kiểm tra xác thực hoặc hạn chế truy cập dữ liệu. 

filter := bson.M{
  "$or": []bson.M{
    {"role": "user"},
    {"active": true},
  },
}

Nếu đầu vào người dùng trực tiếp thao túng mệnh đề $or, kẻ tấn công có thể chèn điều kiện trái phép:

{ "$or": [ {"role": "user"}, {"active": true}, {"isAdmin": true} ] }

Truy vấn đã sửa đổi này có thể lách kiểm soát truy cập dựa trên vai trò, làm lộ dữ liệu quản trị hoặc cấp đặc quyền cao cho người dùng không được phép. Ngăn việc sửa đổi động các toán tử logic bằng cách áp dụng kiểm tra đầu vào nghiêm ngặt và tránh để người dùng điều khiển trực tiếp cấu trúc truy vấn. 

GET /search?filter={"price":{"$gt":0}}

Nếu không có kiểm tra đầu vào nghiêm ngặt, cách làm này cho phép kẻ tấn công thao túng cấu trúc truy vấn và lách kiểm soát bảo mật, có thể làm lộ dữ liệu trái phép. 

Giảm thiểu:

  • Thiết lập danh sách cho phép cho các trường và toán tử hợp lệ, từ chối mọi tham số truy vấn nằm ngoài phạm vi đã định nghĩa.
  • Buộc so khớp bằng nhau tường minh bằng cách bao bọc toàn bộ đầu vào người dùng trong toán tử $eq để ngăn thay thế bằng các toán tử so sánh hoặc logic khác.

Tính năng thực thi JavaScript

MongoDB cung cấp một số khả năng thực thi JavaScript phía máy chủ có thể tạo rủi ro bảo mật, bao gồm:

Các tính năng này tạo ra bề mặt phơi nhiễm bảo mật đáng kể và nên tránh trừ khi yêu cầu nghiệp vụ then chốt bắt buộc dùng. Để ngăn bị khai thác, hãy vô hiệu hoá scripting trong cấu hình MongoDB:

Với mongod:

--noscripting

Hoặc qua tệp cấu hình:

security:
  javascriptEnabled: false

Áp dụng cùng cấu hình cho mongos trong cụm phân mảnh (sharded).

Các toán tử truy vấn gốc của MongoDB cung cấp đầy đủ chức năng, loại bỏ nhu cầu thực thi JavaScript trong hầu hết mọi trường hợp sử dụng. Hoàn toàn tránh dùng mệnh đề $where trừ khi bạn kiểm soát tuyệt đối việc kiểm tra đầu vào và ngữ cảnh thực thi.

Cách ngăn chặn NoSQL Injection

1. Kiểm tra đầu vào nghiêm ngặt

Áp dụng kiểm tra kiểu dữ liệu chặt chẽ, chỉ chấp nhận các kiểu nguyên thuỷ dự kiến như chuỗi, số nguyên hoặc boolean. Từ chối mọi đối tượng lồng nhau, mảng hoặc tên trường không nhận diện được có thể chứa thao tác nhúng hoặc cấu trúc độc hại.

type LoginInput struct {
    Username string json:"username" validate:"required,alphanum"
    Password string json:"password" validate:"required,min=8"
}

2. Đừng tin cấu trúc truy vấn do người dùng cung cấp

Xây dựng mọi truy vấn cơ sở dữ liệu bằng ánh xạ trường và mẫu truy vấn được ứng dụng kiểm soát, định nghĩa sẵn. Không cho phép đầu vào người dùng ảnh hưởng đến kiến trúc truy vấn, toán tử logic hoặc cấu trúc điều kiện có thể làm sai lệch mẫu truy cập dữ liệu dự kiến.

filter := bson.M{
    "username": input.Username,
    "password": input.Password,
}

Tránh:

// Dangerous: allows injection of operators
_ = json.NewDecoder(req.Body).Decode(&filter)

Hầu hết driver MongoDB cung cấp các trình xây dựng truy vấn kiểu mạnh, ép cấu trúc truy vấn và ngăn lỗ hổng injection do vô ý. Tận dụng các giao diện gốc này thay vì phơi bày map truy vấn thô hoặc đối tượng BSON qua các endpoint của ứng dụng.

filter := bson.M{
    "username": input.Username,
    "password": input.Password,
}

Tránh:

// Dangerous: allows injection of operators
_ = json.NewDecoder(req.Body).Decode(&filter)

3. Chặn chèn toán tử MongoDB

Ngăn đầu vào do người dùng điều khiển hoạt động như toán tử truy vấn hoặc cấu trúc logic. Xem mọi dữ liệu người dùng là giá trị chữ (literal) thay vì thành phần truy vấn có thể thực thi.

Thay vì:

filter := bson.M{
    "status": input.Status,
}

Hãy dùng:

filter := bson.M{
    "status": bson.M{"$eq": input.Status},
}

Cách tiếp cận này buộc so khớp bằng nhau nghiêm ngặt và ngăn việc thực thi các toán tử được chèn như $ne, $in hoặc $regex hay các bộ điều chỉnh logic khác có thể lách logic truy vấn dự kiến.

4. Áp dụng đặc quyền tối thiểu cho người dùng cơ sở dữ liệu

Cấu hình kết nối cơ sở dữ liệu của ứng dụng bằng các tài khoản chỉ có quyền tối thiểu cần thiết cho các thao tác dự kiến. Tránh sử dụng thông tin xác thực cấp quản trị trong cấu hình ứng dụng để hạn chế thiệt hại tiềm ẩn khi một cuộc tấn công injection thành công.

5. Giám sát và kiểm thử

Triển khai ghi log toàn diện để phát hiện mẫu truy vấn bất thường và thiết lập quy trình kiểm thử bảo mật định kỳ. Xác thực độ an toàn của endpoint bằng cách thử các cuộc tấn công injection với payload sai lệch như:

{ "$ne": null }

Xác minh rằng ứng dụng từ chối nó.

Các trường hợp kiểm thử nên cân nhắc:

  • Đối tượng thay vì chuỗi: { "username": { "$ne": null } }
  • Mảng thay vì giá trị vô hướng: { "roles": [ { "$gt": "" } ] }
  • Toán tử làm giá trị truy vấn: { "field": { "$regex": ".*" } }

Danh sách kiểm tra tóm tắt

  • Kiểm tra đầu vào: Ép kiểu dữ liệu nghiêm ngặt, xác thực định dạng và từ chối trường không mong đợi.
  • Xây dựng truy vấn: Tạo mọi truy vấn bằng schema định sẵn và mẫu do ứng dụng kiểm soát
  • Bảo vệ toán tử: Chặn toán tử MongoDB trong đầu vào bằng cách bọc giá trị trong $eq hoặc dùng API kiểu hoá.
  • JavaScript phía máy chủ: Loại bỏ mệnh đề $where và vô hiệu hoá thực thi JavaScript trong cấu hình sản xuất.
  • Kiểm soát truy cập DB: Cấu hình kết nối ứng dụng với quyền tối thiểu cần thiết, tránh dùng thông tin quản trị.
  • Kiểm thử và giám sát: Thực hiện kiểm thử injection định kỳ và ghi log các mẫu truy vấn bất thường.

Kết luận

Lỗ hổng NoSQL injection hoàn toàn có thể phòng tránh bằng kỷ luật thực hành mã hoá an toàn. Áp dụng kiểm tra đầu vào nghiêm ngặt, xây dựng truy vấn có cấu trúc và các kiểm soát bảo mật nhiều lớp giúp nhà phát triển duy trì tính linh hoạt, hiệu năng của ứng dụng đồng thời loại bỏ các hướng tấn công dựa trên injection.

Thành công đòi hỏi hiểu rõ luồng dữ liệu hoàn chỉnh của ứng dụng—từ bước parse request ban đầu đến khi thực thi truy vấn cuối cùng. Thiết lập cách tiếp cận không tin cậy mặc định với mọi nguồn đầu vào, thực hiện kiểm tra tại ranh giới hệ thống và ưu tiên nguyên tắc lập trình phòng thủ xuyên suốt toàn bộ stack công nghệ.

Câu hỏi thường gặp

Tại sao NoSQL injection vẫn là rủi ro nếu MongoDB dùng BSON thay vì chuỗi

Injection vẫn có thể xảy ra trước khi đối tượng BSON được tạo. Nếu đầu vào người dùng được parse thành các kiểu linh hoạt như map[string]interface{} hoặc interface{} mà không kiểm tra, kẻ tấn công có thể chèn các toán tử MongoDB như $ne hoặc $or và thao túng logic truy vấn.

Có an toàn không nếu decode thân request trực tiếp vào bson.M hoặc map[string]interface{}?

Không. Cách này cho phép người dùng chèn toán tử truy vấn tuỳ ý. Luôn ánh xạ dữ liệu request sang các kiểu chặt chẽ (ví dụ struct trong Go) và kiểm tra đầu vào trước khi xây dựng truy vấn.

Dùng $where có vấn đề gì nếu đầu vào đã được kiểm tra?

Ngay cả khi một phần đầu vào đã được kiểm tra, $where vẫn thực thi JavaScript trên máy chủ. Bất kỳ sai sót nào trong escaping hoặc kiểm tra cũng có thể dẫn đến chèn mã. An toàn hơn là tránh hẳn $where hoặc vô hiệu hoá scripting trong cấu hình MongoDB.

Làm sao kiểm thử endpoint của tôi có dễ bị NoSQL injection không?

Hãy thử chèn các payload như:

{ "username": { "$ne": null } }

Nếu endpoint trả về phản hồi hợp lệ hoặc lách được logic (ví dụ đăng nhập), nghĩa là đầu vào chưa được kiểm tra hoặc ràng buộc đúng mức.

Cách tốt nhất để xây dựng truy vấn an toàn trong Go là gì?

Sử dụng struct kiểu hoá cho đầu vào, tránh decode vào map tổng quát, và bọc các giá trị bằng $eq khi cần. Không bao giờ để người dùng định nghĩa toán tử hoặc chèn một phần truy vấn trực tiếp.

Chủ đề

Liên quan