diff --git a/README.md b/README.md index ad10fed..2ca92a7 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# sql-error \ No newline at end of file +# sql-error + +Kho này tổng hợp các ghi chú ngắn bằng tiếng Việt về những lỗ hổng và rủi ro vận hành liên quan đến SQL. Mỗi chủ đề được tách thành một file Markdown riêng trong thư mục `vulnerabilities/`. + +## Lưu ý sử dụng + +Các ví dụ trong thư mục `vulnerabilities/` chỉ nhằm mục đích học tập và rà soát bảo mật nội bộ. Không triển khai các đoạn code được đánh dấu là không an toàn trong môi trường thật. + +## Danh sách chủ đề + +- [SQL Injection](vulnerabilities/sql-injection.md) +- [Blind SQL Injection](vulnerabilities/blind-sql-injection.md) +- [Second-Order SQL Injection](vulnerabilities/second-order-sql-injection.md) +- [Truy vấn SQL quá phức tạp gây quá tải](vulnerabilities/excessive-query-complexity.md) +- [Truy vấn không giới hạn kết quả](vulnerabilities/unbounded-result-sets.md) +- [Lock Contention và Deadlock trong SQL](vulnerabilities/lock-contention-deadlocks.md) +- [Thiếu Rate Limit cho thao tác SQL nặng](vulnerabilities/missing-rate-limits.md) diff --git a/vulnerabilities/blind-sql-injection.md b/vulnerabilities/blind-sql-injection.md new file mode 100644 index 0000000..4ee63d6 --- /dev/null +++ b/vulnerabilities/blind-sql-injection.md @@ -0,0 +1,46 @@ +# Blind SQL Injection + +## Tóm tắt +Blind SQL Injection là biến thể của SQL Injection trong đó ứng dụng không trả trực tiếp dữ liệu hoặc lỗi SQL, nhưng phản hồi vẫn để lộ tín hiệu gián tiếp như đúng/sai, thời gian phản hồi hoặc khác biệt nội dung trang. + +## Tác động +- Trích xuất dữ liệu từng phần thông qua quan sát phản hồi. +- Gây chậm hệ thống bằng các truy vấn có điều kiện tốn thời gian. +- Khó phát hiện hơn SQL Injection thông thường vì không nhất thiết sinh lỗi rõ ràng. + +## Nguyên nhân phổ biến +- Vẫn tồn tại truy vấn động không tham số hóa. +- Cơ chế xử lý lỗi bị ẩn nhưng logic phản hồi vẫn phụ thuộc vào kết quả SQL. +- Thiếu giới hạn thời gian thực thi truy vấn và giám sát bất thường. + +## Biện pháp phòng tránh +- Dùng parameterized query cho mọi dữ liệu đầu vào. +- Chuẩn hóa phản hồi lỗi để không tiết lộ trạng thái truy vấn. +- Đặt timeout cho truy vấn và giới hạn tài nguyên mỗi phiên/kết nối. +- Theo dõi các mẫu truy vấn lặp lại, phản hồi chậm bất thường hoặc tỷ lệ lỗi cao. + +## Ví dụ code minh họa + +Ví dụ **không an toàn** sau không trả lỗi SQL trực tiếp, nhưng phản hồi `exists` vẫn phụ thuộc vào truy vấn được ghép chuỗi từ input. Khác biệt đúng/sai trong phản hồi có thể trở thành tín hiệu gián tiếp. + +```js +app.get('/check-user', async (req, res) => { + const username = req.query.username; + const sql = "SELECT id FROM users WHERE username = '" + username + "'"; + const rows = await db.query(sql); + res.json({ exists: rows.length > 0 }); +}); +``` + +Phiên bản an toàn dùng tham số hóa và phản hồi được chuẩn hóa theo nhu cầu nghiệp vụ. + +```js +app.get('/check-user', async (req, res) => { + const username = req.query.username; + const rows = await db.query( + 'SELECT id FROM users WHERE username = ?', + [username] + ); + res.json({ exists: rows.length > 0 }); +}); +``` diff --git a/vulnerabilities/excessive-query-complexity.md b/vulnerabilities/excessive-query-complexity.md new file mode 100644 index 0000000..a5614c2 --- /dev/null +++ b/vulnerabilities/excessive-query-complexity.md @@ -0,0 +1,56 @@ +# Truy vấn SQL quá phức tạp gây quá tải + +## Tóm tắt +Một hệ thống có thể bị quá tải khi cho phép người dùng tạo bộ lọc, sắp xếp, tìm kiếm hoặc báo cáo dẫn đến truy vấn SQL quá phức tạp. Dù không phải lúc nào cũng là xâm nhập dữ liệu, đây là rủi ro gây từ chối dịch vụ ở tầng cơ sở dữ liệu. + +## Tác động +- CPU, RAM hoặc I/O của database tăng cao. +- Hàng đợi kết nối bị đầy, làm chậm toàn bộ ứng dụng. +- Khóa bảng hoặc khóa dòng kéo dài, ảnh hưởng tác vụ hợp lệ. +- Chi phí hạ tầng tăng do truy vấn không tối ưu. + +## Nguyên nhân phổ biến +- Cho phép lọc tùy ý trên nhiều cột không có index phù hợp. +- Tìm kiếm dùng wildcard rộng trên tập dữ liệu lớn. +- JOIN nhiều bảng lớn không giới hạn phạm vi. +- Không có phân trang, giới hạn số dòng hoặc timeout truy vấn. + +## Biện pháp phòng tránh +- Đặt giới hạn bắt buộc cho phân trang, kích thước kết quả và độ phức tạp bộ lọc. +- Tạo index dựa trên mẫu truy vấn thực tế và kiểm tra kế hoạch thực thi. +- Dùng cache, materialized view hoặc hàng đợi cho báo cáo nặng. +- Thiết lập statement timeout, connection pool hợp lý và rate limit theo người dùng. +- Theo dõi slow query log và cảnh báo khi tài nguyên tăng bất thường. + +## Ví dụ code minh họa + +Ví dụ **không an toàn** sau cho phép client gửi danh sách cột cần tìm kiếm tùy ý. Khi số cột lớn hoặc cột không có index, truy vấn có thể trở nên rất nặng. + +```js +app.get('/search', async (req, res) => { + const columns = req.query.columns.split(','); + const keyword = `%${req.query.q}%`; + const where = columns.map((column) => `${column} LIKE ?`).join(' OR '); + const rows = await db.query(`SELECT * FROM products WHERE ${where}`, columns.map(() => keyword)); + res.json(rows); +}); +``` + +Cách an toàn hơn là dùng allowlist cột được phép tìm kiếm, giới hạn số điều kiện và giới hạn kết quả. + +```js +const SEARCHABLE_COLUMNS = new Set(['name', 'sku']); + +app.get('/search', async (req, res) => { + const requestedColumns = req.query.columns.split(',').slice(0, 2); + const columns = requestedColumns.filter((column) => SEARCHABLE_COLUMNS.has(column)); + const keyword = `%${req.query.q.slice(0, 100)}%`; + const where = columns.map((column) => `${column} LIKE ?`).join(' OR '); + + const rows = await db.query( + `SELECT id, name, sku FROM products WHERE ${where} LIMIT 50`, + columns.map(() => keyword) + ); + res.json(rows); +}); +``` diff --git a/vulnerabilities/lock-contention-deadlocks.md b/vulnerabilities/lock-contention-deadlocks.md new file mode 100644 index 0000000..f009554 --- /dev/null +++ b/vulnerabilities/lock-contention-deadlocks.md @@ -0,0 +1,66 @@ +# Lock Contention và Deadlock trong SQL + +## Tóm tắt +Lock contention xảy ra khi nhiều giao dịch tranh chấp cùng tài nguyên dữ liệu. Deadlock xảy ra khi các giao dịch chờ lẫn nhau và không thể tiếp tục nếu hệ quản trị cơ sở dữ liệu không hủy một giao dịch. Cả hai đều có thể làm giảm hiệu năng hoặc gây gián đoạn dịch vụ. + +## Tác động +- Tăng thời gian phản hồi của truy vấn và giao dịch. +- Giao dịch hợp lệ bị rollback do deadlock. +- Hàng đợi kết nối tăng, làm ứng dụng chậm hoặc lỗi timeout. +- Người dùng gặp lỗi không ổn định khi tải cao. + +## Nguyên nhân phổ biến +- Giao dịch giữ khóa quá lâu do xử lý nhiều logic trong transaction. +- Cập nhật bảng theo thứ tự không nhất quán giữa các luồng nghiệp vụ. +- Thiếu index khiến lệnh cập nhật phải quét và khóa phạm vi lớn. +- Mức cô lập giao dịch quá cao so với nhu cầu thực tế. + +## Biện pháp phòng tránh +- Giữ transaction ngắn và tránh gọi dịch vụ bên ngoài trong transaction. +- Cập nhật tài nguyên theo thứ tự nhất quán trong toàn hệ thống. +- Bổ sung index cho điều kiện WHERE trong UPDATE và DELETE. +- Thiết kế cơ chế retry an toàn cho lỗi deadlock có thể khôi phục. +- Theo dõi lock wait, deadlock report và truy vấn giữ khóa lâu. + +## Ví dụ code minh họa + +Ví dụ **không an toàn** sau giữ transaction trong lúc thực hiện logic chậm. Khóa dữ liệu có thể bị giữ lâu hơn cần thiết và làm các giao dịch khác phải chờ. + +```js +async function updateInventory(orderId) { + await db.beginTransaction(); + const items = await db.query('SELECT product_id, quantity FROM order_items WHERE order_id = ?', [orderId]); + + for (const item of items) { + await db.query( + 'UPDATE products SET stock = stock - ? WHERE id = ?', + [item.quantity, item.product_id] + ); + await callExternalWarehouseApi(item.product_id); // Không nên gọi dịch vụ ngoài trong transaction. + } + + await db.commit(); +} +``` + +Cách tốt hơn là chuẩn bị dữ liệu trước, cập nhật theo thứ tự nhất quán và giữ transaction ngắn. + +```js +async function updateInventory(orderId) { + const items = await db.query( + 'SELECT product_id, quantity FROM order_items WHERE order_id = ? ORDER BY product_id', + [orderId] + ); + + await db.beginTransaction(); + for (const item of items) { + await db.query( + 'UPDATE products SET stock = stock - ? WHERE id = ?', + [item.quantity, item.product_id] + ); + } + await db.commit(); + + await notifyWarehouseAfterCommit(items); +} +``` diff --git a/vulnerabilities/missing-rate-limits.md b/vulnerabilities/missing-rate-limits.md new file mode 100644 index 0000000..594ecdb --- /dev/null +++ b/vulnerabilities/missing-rate-limits.md @@ -0,0 +1,53 @@ +# Thiếu Rate Limit cho thao tác SQL nặng + +## Tóm tắt +Khi các endpoint kích hoạt truy vấn SQL nặng không có rate limit, một người dùng hoặc bot có thể gửi nhiều yêu cầu liên tiếp làm quá tải database. Đây là rủi ro vận hành thường gặp ở tìm kiếm, báo cáo, thống kê và export dữ liệu. + +## Tác động +- Tài nguyên database bị chiếm dụng bởi một nhóm nhỏ yêu cầu. +- Người dùng hợp lệ bị chậm hoặc không thể truy cập dịch vụ. +- Tăng nguy cơ timeout, lỗi kết nối và tự động mở rộng tốn kém. +- Khó phân biệt giữa tăng trưởng thật và lạm dụng nếu thiếu quan sát. + +## Nguyên nhân phổ biến +- Không giới hạn số lần gọi endpoint theo tài khoản, IP hoặc token. +- Không phân loại độ nặng của truy vấn để áp dụng quota khác nhau. +- Cho phép chạy báo cáo đồng bộ nhiều lần trong thời gian ngắn. +- Thiếu circuit breaker khi database đã quá tải. + +## Biện pháp phòng tránh +- Áp dụng rate limit và quota riêng cho endpoint tốn tài nguyên. +- Dùng hàng đợi, cache hoặc lịch chạy cho báo cáo lớn. +- Từ chối hoặc trì hoãn yêu cầu khi database vượt ngưỡng tải an toàn. +- Ghi log người dùng, tham số truy vấn và thời gian thực thi để điều tra lạm dụng. + +## Ví dụ code minh họa + +Ví dụ **không an toàn** sau cho phép gọi báo cáo nặng liên tục. Mỗi request đều chạy truy vấn tổng hợp lớn trực tiếp trên database. + +```js +app.get('/reports/revenue', async (req, res) => { + const rows = await db.query(` + SELECT customer_id, SUM(total) AS revenue + FROM orders + WHERE created_at >= ? AND created_at < ? + GROUP BY customer_id + ORDER BY revenue DESC + `, [req.query.from, req.query.to]); + + res.json(rows); +}); +``` + +Cách an toàn hơn là áp dụng rate limit, giới hạn khoảng thời gian và chuyển báo cáo nặng sang hàng đợi hoặc cache. + +```js +app.get('/reports/revenue', reportRateLimit, async (req, res) => { + const range = normalizeDateRange(req.query.from, req.query.to, { maxDays: 31 }); + const cached = await reportCache.get(range.cacheKey); + if (cached) return res.json(cached); + + const job = await reportQueue.enqueue('revenue-report', range); + res.status(202).json({ jobId: job.id, status: 'queued' }); +}); +``` diff --git a/vulnerabilities/second-order-sql-injection.md b/vulnerabilities/second-order-sql-injection.md new file mode 100644 index 0000000..c605efd --- /dev/null +++ b/vulnerabilities/second-order-sql-injection.md @@ -0,0 +1,53 @@ +# Second-Order SQL Injection + +## Tóm tắt +Second-Order SQL Injection xảy ra khi dữ liệu độc hại được lưu vào cơ sở dữ liệu ở một bước trước, sau đó được dùng lại trong truy vấn SQL ở bước khác mà không được tham số hóa hoặc kiểm soát. + +## Tác động +- Bypass các kiểm tra ở điểm nhập ban đầu vì lỗi chỉ xuất hiện ở luồng xử lý sau. +- Ảnh hưởng đến tác vụ nền, báo cáo, quản trị hoặc đồng bộ dữ liệu. +- Có thể dẫn đến rò rỉ hoặc sửa đổi dữ liệu tùy vị trí dữ liệu được tái sử dụng. + +## Nguyên nhân phổ biến +- Tin tưởng dữ liệu đã lưu trong database là an toàn. +- Tạo truy vấn động từ trường cấu hình, tên người dùng, bộ lọc hoặc metadata. +- Không áp dụng cùng tiêu chuẩn bảo vệ cho tác vụ nội bộ và tác vụ nền. + +## Biện pháp phòng tránh +- Tham số hóa truy vấn ở mọi nơi, kể cả khi dữ liệu đến từ database nội bộ. +- Xác thực và chuẩn hóa dữ liệu trước khi lưu và trước khi tái sử dụng. +- Rà soát các chức năng tạo báo cáo, tìm kiếm nâng cao và tác vụ batch. +- Tách quyền giữa tài khoản ứng dụng, tài khoản quản trị và tài khoản tác vụ nền. + +## Ví dụ code minh họa + +Ở bước đầu, ứng dụng lưu `displayName` vào database. Lỗi xuất hiện ở bước sau khi dữ liệu đã lưu được dùng lại để tạo truy vấn động. + +```js +async function saveProfile(userId, displayName) { + await db.query( + 'UPDATE users SET display_name = ? WHERE id = ?', + [displayName, userId] + ); +} + +async function buildAuditReport(userId) { + const user = await db.query('SELECT display_name FROM users WHERE id = ?', [userId]); + const displayName = user[0].display_name; + + // Không an toàn: dữ liệu lấy từ database vẫn có thể là dữ liệu không đáng tin cậy. + return db.query("SELECT * FROM audit_logs WHERE actor = '" + displayName + "'"); +} +``` + +Cần tiếp tục tham số hóa cả khi dữ liệu đến từ database nội bộ. + +```js +async function buildAuditReport(userId) { + const user = await db.query('SELECT display_name FROM users WHERE id = ?', [userId]); + return db.query( + 'SELECT * FROM audit_logs WHERE actor = ?', + [user[0].display_name] + ); +} +``` diff --git a/vulnerabilities/sql-injection.md b/vulnerabilities/sql-injection.md new file mode 100644 index 0000000..42d40d6 --- /dev/null +++ b/vulnerabilities/sql-injection.md @@ -0,0 +1,49 @@ +# SQL Injection + +## Tóm tắt +SQL Injection xảy ra khi ứng dụng ghép trực tiếp dữ liệu người dùng vào câu lệnh SQL mà không kiểm soát đúng cách. Kẻ tấn công có thể làm thay đổi ý nghĩa truy vấn, đọc dữ liệu trái phép, sửa dữ liệu hoặc bỏ qua cơ chế xác thực. + +## Tác động +- Rò rỉ dữ liệu nhạy cảm như tài khoản, email, token hoặc thông tin thanh toán. +- Thay đổi, xóa hoặc chèn dữ liệu trái phép. +- Vượt qua đăng nhập nếu truy vấn xác thực bị thao túng. +- Làm gián đoạn dịch vụ khi truy vấn bị biến thành tác vụ tốn tài nguyên. + +## Nguyên nhân phổ biến +- Nối chuỗi SQL bằng input từ người dùng. +- Không dùng prepared statement hoặc parameterized query. +- Tài khoản kết nối cơ sở dữ liệu có quyền quá rộng. +- Thông báo lỗi SQL trả về quá chi tiết cho người dùng cuối. + +## Biện pháp phòng tránh +- Luôn dùng prepared statement, parameterized query hoặc ORM an toàn. +- Kiểm tra kiểu dữ liệu, độ dài và định dạng input ở tầng ứng dụng. +- Áp dụng nguyên tắc đặc quyền tối thiểu cho tài khoản cơ sở dữ liệu. +- Ẩn chi tiết lỗi SQL khỏi phản hồi công khai; ghi log nội bộ có kiểm soát. +- Bổ sung kiểm thử bảo mật cho các endpoint nhận input động. + +## Ví dụ code minh họa + +Ví dụ dưới đây **không an toàn** vì giá trị `email` được ghép trực tiếp vào chuỗi SQL. Nếu input chứa ký tự điều khiển SQL, cấu trúc truy vấn có thể bị thay đổi ngoài ý muốn. + +```js +app.get('/users', async (req, res) => { + const email = req.query.email; + const sql = "SELECT id, email, role FROM users WHERE email = '" + email + "'"; + const rows = await db.query(sql); + res.json(rows); +}); +``` + +Cách viết an toàn hơn là dùng tham số hóa để database driver xử lý input như dữ liệu, không phải một phần của câu lệnh SQL. + +```js +app.get('/users', async (req, res) => { + const email = req.query.email; + const rows = await db.query( + 'SELECT id, email, role FROM users WHERE email = ?', + [email] + ); + res.json(rows); +}); +``` diff --git a/vulnerabilities/unbounded-result-sets.md b/vulnerabilities/unbounded-result-sets.md new file mode 100644 index 0000000..6106591 --- /dev/null +++ b/vulnerabilities/unbounded-result-sets.md @@ -0,0 +1,50 @@ +# Truy vấn không giới hạn kết quả + +## Tóm tắt +Truy vấn không giới hạn kết quả xảy ra khi API hoặc chức năng xuất dữ liệu cho phép trả về quá nhiều dòng cùng lúc. Điều này có thể làm cạn bộ nhớ ứng dụng, bão hòa mạng hoặc gây áp lực lớn lên cơ sở dữ liệu. + +## Tác động +- Tăng độ trễ hoặc timeout cho người dùng hợp lệ. +- Cạn RAM ở ứng dụng khi cố nạp toàn bộ kết quả vào bộ nhớ. +- Làm nghẽn băng thông giữa ứng dụng và database. +- Dễ bị lạm dụng để tạo tải cao lặp lại. + +## Nguyên nhân phổ biến +- Thiếu LIMIT, OFFSET hoặc cursor-based pagination. +- Endpoint export dữ liệu không có giới hạn hoặc không chạy bất đồng bộ. +- Không giới hạn tần suất gọi API theo người dùng hoặc IP. +- Không có ngưỡng tối đa cho khoảng thời gian lọc dữ liệu. + +## Biện pháp phòng tránh +- Bắt buộc phân trang và đặt kích thước trang tối đa. +- Dùng streaming hoặc tác vụ nền cho export lớn thay vì trả đồng bộ. +- Giới hạn khoảng thời gian truy vấn và số bản ghi tối đa. +- Áp dụng rate limit, quota và giám sát tải theo endpoint. + +## Ví dụ code minh họa + +Ví dụ **không an toàn** sau trả toàn bộ dữ liệu khớp điều kiện. Nếu bảng lớn, ứng dụng có thể tiêu tốn nhiều RAM và database phải đọc quá nhiều dòng. + +```js +app.get('/orders', async (req, res) => { + const rows = await db.query( + 'SELECT * FROM orders WHERE customer_id = ?', + [req.query.customerId] + ); + res.json(rows); +}); +``` + +Phiên bản an toàn đặt giới hạn trang tối đa và chỉ chọn các cột cần thiết. + +```js +app.get('/orders', async (req, res) => { + const limit = Math.min(Number(req.query.limit) || 50, 100); + const offset = Math.max(Number(req.query.offset) || 0, 0); + const rows = await db.query( + 'SELECT id, status, total, created_at FROM orders WHERE customer_id = ? ORDER BY id LIMIT ? OFFSET ?', + [req.query.customerId, limit, offset] + ); + res.json(rows); +}); +```