SQL Injection vs NoSQL Injection: Prisma và TypeORM bảo vệ bạn đến đâu?
Hướng dẫn bảo mật query với Prisma và TypeORM: prepared statements, raw queries, dynamic identifiers, NoSQL operators, mass assignment, least privilege, review và testing.
ShanverseUpdated 10 min read— viewsLoading views
Injection
API Security
Injection xảy ra khi dữ liệu biến thành câu lệnh
Nguyên tắc chung của mọi injection là ranh giới giữa code và data bị phá vỡ. Ứng dụng dự định gửi một giá trị, nhưng interpreter lại hiểu một phần giá trị đó như cú pháp truy vấn.
Với SQL, developer có thể viết:
Nếu email chứa dấu nháy và SQL fragment, cấu trúc câu lệnh có thể bị thay đổi. Hậu quả không chỉ là bypass login: attacker có thể đọc dữ liệu, sửa/xóa dữ liệu, mở rộng truy vấn, khai thác blind/timing channel hoặc tận dụng quyền quá lớn của database account.
Với NoSQL, injection thường không cần “thoát khỏi chuỗi SQL”. Attacker có thể đưa operator hoặc cấu trúc object vào filter:
Nếu driver/ODM chấp nhận object này,
Comments
$ne
không còn là dữ liệu; nó là toán tử truy vấn. NoSQL Injection vì vậy là injection vào
query language, selector, expression hoặc pipeline
, không phải bằng chứng rằng NoSQL “không có schema nên không thể injection”.
SQL Injection và NoSQL Injection khác nhau thế nào?
Cả hai đều tuân theo một quy tắc: không cho input quyết định cấu trúc query. Input chỉ được phép trở thành value đã được validate.
Parameterized query bảo vệ như thế nào?
Prepared statement tách SQL template và parameters:
Driver gửi email như value của $1. Dù value chứa ' OR 1=1 --, database vẫn xử lý nó như chuỗi, không ghép vào grammar của SQL.
Điều này hiệu quả cho values, nhưng không tự parameterize được mọi thành phần cú pháp như:
Tên bảng hoặc cột.
ASC / DESC.
SQL keyword hoặc operator.
Một fragment WHERE hoàn chỉnh.
Số lượng/kiểu join do input quyết định.
Các phần cấu trúc phải được chọn từ allowlist do server sở hữu.
Prisma bảo vệ bạn ở đâu?
Prisma Client sinh API có cấu trúc:
email được truyền như value thông qua query engine/driver, không được nối vào SQL string. Các thao tác findMany, create, update, relation filters và nhiều API thông thường an toàn hơn raw SQL vì model, field và operation được biểu diễn bằng cấu trúc đã biết.
Prisma an toàn với tagged raw query
Khi thật sự cần SQL, dùng tagged template đúng cách:
Interpolation ở đây được chuyển thành parameter cho value. Nhưng không dùng interpolation cho identifier:
Nếu cần bảng/cột động, ánh xạ một key công khai sang fragment nội bộ đã cố định, hoặc tạo các query riêng.
Prisma vẫn có thể bị SQL Injection
Các API có chữ Unsafe làm tăng rủi ro rõ rệt:
Tương tự với $executeRawUnsafe. Một lỗi khác là tạo query string trước rồi truyền vào raw API, hoặc cố “giả” tagged template theo cách phá cơ chế escaping. Quy tắc code review đơn giản:
Cấm *RawUnsafe theo mặc định.
Mọi ngoại lệ phải có owner, lý do và security review.
Không cho user input xuất hiện trong SQL text.
Ưu tiên Prisma Client API hoặc TypedSQL/tagged templates.
TypeORM bảo vệ bạn ở đâu?
Repository methods truyền values qua driver:
QueryBuilder an toàn khi dùng named parameters:
TypeORM thay :email bằng database parameter. Ngược lại, nối chuỗi phá ranh giới code/data:
Raw query() cũng cần placeholder đúng theo database driver:
Placeholder có thể là $1, ?, :1, @0 tùy database. Đọc API/driver cụ thể thay vì tự đoán.
Raw expressions trong TypeORM
Một số API cho phép function-style value hoặc SQL expression:
Đây là SQL code, không phải value được escape. Chỉ dùng expression tĩnh do developer kiểm soát. Không đưa input vào callback raw.
Dynamic sorting: nơi ORM thường bị dùng sai
Parameters không thay thế identifiers hoặc keyword:
Dùng mapping cố định:
Áp dụng cùng nguyên tắc cho table, column, relation, operator, aggregate, groupBy và selected fields. Không “sanitize” identifier bằng cách xóa vài ký tự; allowlist semantic choice tốt hơn blacklist.
Dynamic filter và API tìm kiếm
Một endpoint linh hoạt dễ trở thành query-language proxy:
Ngay cả khi không tạo SQL Injection truyền thống, attacker có thể:
Truy vấn field không được phép.
Lọc theo relation nhạy cảm.
Tạo filter sâu gây DoS.
Suy luận dữ liệu tồn tại qua timing/count.
Bypass tenant condition nếu object merge sai.
Xây DTO nhỏ và dựng query server-side:
Đặc biệt chú ý thứ tự spread:
Injection prevention không thay thế authorization. ORM có thể parameterize hoàn hảo nhưng vẫn trả dữ liệu tenant khác nếu filter authorization sai.
NoSQL Injection trong MongoDB-style query
Ví dụ login nguy hiểm:
Nếu API parser cho phép object thay vì string, attacker có thể gửi operator. Phòng thủ:
Validate type chứ không chỉ kiểm tra field tồn tại.
Reject unknown keys bằng strict schema.
Tạo filter mới, không truyền/spread object từ request.
Không cho keys bắt đầu bằng $ hoặc chứa . nếu API không chủ đích hỗ trợ chúng.
Không dùng eval, string-to-query parser hoặc $where với input.
Giới hạn regex length/complexity; regex không an toàn có thể gây ReDoS hoặc scan toàn collection.
Với aggregation pipeline, allowlist stages và fields; không nhận pipeline tùy ý từ client.
NoSQL không đồng nghĩa với MongoDB
Injection có thể xuất hiện ở Elasticsearch query DSL, Redis/Lua, graph query, Firebase rules, Cassandra CQL hoặc search filters. Query object vẫn là một ngôn ngữ. Nếu client được phép gửi AST hoặc JSON DSL gần như nguyên bản, bạn đã trao một phần interpreter cho bên không đáng tin.
Thiết kế API theo intent:
thay vì public database DSL:
Escaping có đủ không?
Escaping thủ công là lựa chọn yếu vì phụ thuộc dialect, encoding, character set và context. Prepared statements/structured APIs nên là mặc định. Escaping chỉ là phương án cuối khi interpreter không có parameter API và phải dùng routine chính thức của driver—not một hàm replace("'", "''") tự viết.
ORM không ngăn mass assignment
Đoạn code này có thể không injection nhưng vẫn nguy hiểm:
Attacker có thể gửi role, isAdmin, balance hoặc tenantId. Chọn field rõ ràng:
TypeScript type không tồn tại ở runtime. Một as UserUpdateInput không validate request.
Least privilege giảm blast radius
Nếu injection xảy ra, database account của ứng dụng quyết định thiệt hại:
Runtime account không nên là owner/superuser.
Tách migration role khỏi application role.
Chỉ cấp quyền trên schema/table cần thiết.
Read-only workload dùng credential read-only.
Không cho runtime tạo extension, role hoặc thực thi OS function.
Dùng row-level security như lớp bổ sung khi phù hợp, nhưng vẫn giữ authorization trong application.
Không để database public Internet nếu không cần.
Logging và detection
Không log raw password, token hoặc toàn bộ query parameters. Ghi:
Query fingerprint/template thay vì giá trị nhạy cảm.
Route, operation, model/table, duration và row count.
Validation failure theo loại, không echo payload nguy hiểm.
Việc sử dụng raw-query API trong inventory/code scan.
Alert cho query time tăng đột biến, nhiều lỗi database, response row count bất thường và repeated payload patterns. WAF có thể giảm nhiễu nhưng không thay parameterization.
Testing và code review
Static rules
Tìm $queryRawUnsafe, $executeRawUnsafe, .query( và raw expression callbacks.
Tìm template literal/string concatenation gần SQL keyword.
Tìm where: req.body, object spread từ request và dynamic orderBy.
Tìm $where, $expr, regex và aggregation pipeline từ input.
Dùng SAST/lint rule chặn raw API không được phê duyệt.
Automated tests
Gửi strings chứa quote, comment marker và Unicode edge cases; kết quả phải được coi là data.
Gửi object thay vì string cho NoSQL fields.
Gửi keys $ne, $gt, $regex, $where và dotted paths; schema phải reject.
Test tenant boundary, unauthorized fields và large/deep filters.
Test sort field/order không hợp lệ.
Test database role không thể sửa schema hoặc truy cập bảng ngoài phạm vi.
Chỉ test trên môi trường được cấp phép và dữ liệu không nhạy cảm.
Checklist production
ORM structured API là mặc định.
Mọi value trong raw SQL dùng parameter binding/tagged template đúng cách.
*RawUnsafe bị cấm hoặc cần security review.
Dynamic identifiers, sort, operators và fields dùng exact allowlist.
Request được validate bằng strict runtime schema.
Không truyền/spread req.body trực tiếp vào where, data hoặc pipeline.
Tenant/user authorization được server thêm vào và không thể bị ghi đè.
NoSQL operators, $where, expression và regex bị giới hạn.
Pagination/depth/complexity có giới hạn chống DoS.
Runtime database account theo least privilege.
Logs không chứa secrets hoặc raw payload; có query monitoring.
CI quét raw queries và chạy negative security tests.
Kết luận
Prisma và TypeORM bảo vệ tốt khi bạn dùng generated/repository API và parameterized QueryBuilder. Nhưng ORM chỉ đảm bảo an toàn cho ranh giới mà bạn cho phép nó quản lý. Khi tự dựng SQL text, mở raw expression, nhận query object từ client hoặc để input chọn identifier/operator, bạn đã bước ra khỏi hàng rào đó.
Công thức bền vững là: structured ORM API + parameterized values + allowlisted query structure + strict runtime validation + authorization + least privilege. Đừng hỏi “ORM có chống injection không?”; hãy hỏi “phần nào của query vẫn đang do input quyết định?”.
Table of contents(21)
Codetypescript
1constsql=`SELECT * FROM users WHERE email = '${email}'`
Hướng dẫn tích hợp cổng thanh toán VNPay, ZaloPay, MoMo và Stripe: payment flow, chữ ký webhook/IPN, idempotency, retry, reconciliation, refund và vận hành production.
Hướng dẫn kiến trúc Open Banking và Open API Spec: FAPI 2.0, OAuth 2.0/OIDC, PAR, PKCE, mTLS/DPoP, consent management, TPP onboarding, API security và vận hành.
Hướng dẫn tích hợp VietQR và QR Code động với webhook/IPN từ ngân hàng hoặc cổng thanh toán: xác minh chữ ký, chống replay, matching giao dịch, idempotency và reconciliation.