Как защититься от SQL Injection?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как защититься от SQL Injection?
SQL Injection — одна из самых опасных уязвимостей в веб приложениях. Это атака, когда злоумышленник внедряет SQL код в пользовательский ввод. Я расскажу о всех способах защиты.
Что такое SQL Injection (примеры атак)
// УЯЗВИМЫЙ КОД - НИКОГДА ТАК НЕ ДЕЛАЙ!
const userId = req.query.id; // пользователь передаёт: 1 OR 1=1
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Результат: SELECT * FROM users WHERE id = 1 OR 1=1
// Вернёт ВСЕ пользователей вместо одного!
// Ещё опаснее - удаление данных
// Пользователь передаёт: 1; DROP TABLE users; --
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Результат: SELECT * FROM users WHERE id = 1; DROP TABLE users; --
// Удалит всю таблицу!
Способ 1: Параметризованные запросы (ГЛАВНОЕ ОРУЖИЕ)
Это основной и самый надёжный способ защиты от SQL Injection.
// Правильно - с параметризацией
const userId = req.query.id;
// node-postgres (pg)
const result = await db.query(
'SELECT * FROM users WHERE id = $1',
[userId] // параметр передаётся отдельно
);
// MySQL (mysql2)
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
// Sqlite3
db.get(
'SELECT * FROM users WHERE id = ?',
[userId],
(err, row) => { /* ... */ }
);
Почему это работает:
База данных получает SQL структуру и данные отдельно. Даже если пользователь передаст 1 OR 1=1, база обработает это как строку, а не SQL код.
// TypeORM (ORM автоматически параметризует)
const user = await userRepository.findOne({
where: { id: userId }
});
// Prisma
const user = await prisma.user.findUnique({
where: { id: userId }
});
// Sequelize
const user = await User.findOne({
where: { id: userId }
});
Способ 2: Используй ORM (Object-Relational Mapping)
ORMs автоматически параметризуют запросы.
// TypeORM
const user = await db.getRepository(User)
.createQueryBuilder('user')
.where('user.email = :email', { email: userEmail })
.getOne();
// Или simplest way
const users = await db.getRepository(User).find({
where: { email: userEmail }
});
// Sequelize
const user = await User.findOne({
where: { email: userEmail }
});
// Prisma
const user = await prisma.user.findUnique({
where: { email: userEmail }
});
Способ 3: Валидация входа
Добавь слой валидации перед БД.
import { validate, IsUUID, IsEmail, IsInt } from 'class-validator';
class GetUserDto {
@IsUUID()
id: string; // Только UUID формат
}
class LoginDto {
@IsEmail()
email: string; // Только валидный email
}
// В контроллере
async function getUser(req: Request) {
const dto = new GetUserDto();
dto.id = req.params.id;
const errors = await validate(dto);
if (errors.length > 0) {
throw new BadRequestException('Invalid ID');
}
// Только теперь обращаемся к БД
return await userService.findById(dto.id);
}
Способ 4: Используй prepared statements
// Node.js с pg (PostgreSQL)
const { Pool } = require('pg');
const pool = new Pool();
// Плохо
const query = 'SELECT * FROM users WHERE id = ' + userId;
const result = await pool.query(query);
// Хорошо
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
Способ 5: Санитизация (вспомогательное средство)
Удаление опасных символов. Не заменяет параметризацию!
function escapeSql(str) {
return str.replace(/'/g, "''");
}
// Плохо - всё равно уязвимо
const name = escapeSql(req.body.name);
const query = `SELECT * FROM users WHERE name = '${name}'`; // УЯЗИМО!
// Хорошо - комбинируй с параметризацией
const result = await db.query(
'SELECT * FROM users WHERE name = $1',
[req.body.name]
);
Способ 6: Принцип наименьших привилегий (DB User)
Дай приложению БД пользователя с минимальными правами.
-- Создай отдельного пользователя для приложения
CREATE USER app_user WITH PASSWORD 'strong_password';
-- Дай только необходимые права
GRANT SELECT, INSERT, UPDATE ON public.users TO app_user;
-- НЕ дай права на удаление или DROP
REVOKE DELETE ON public.users FROM app_user;
-- Подключайся в приложении как app_user
// DATABASE_URL=postgres://app_user:pwd@localhost/db
Если взломают приложение, они не смогут удалить БД.
Способ 7: Rate Limiting и мониторинг
import rateLimit from 'express-rate-limit';
// Ограничь количество запросов
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100 // максимум 100 запросов
});
app.use('/api/', limiter);
// Мониторь странные паттерны
app.use((req, res, next) => {
if (req.query.id?.includes('OR') || req.query.id?.includes(';')) {
console.warn('Suspicious SQL pattern detected', req.query);
// Залогируй, заблокируй IP
}
next();
});
Полный пример безопасного кода
import express, { Request, Response } from 'express';
import { Pool } from 'pg';
import { validate, IsUUID } from 'class-validator';
const pool = new Pool();
class GetUserDto {
@IsUUID()
id: string;
}
router.get('/users/:id', async (req: Request, res: Response) => {
try {
// Валидация входа
const dto = new GetUserDto();
dto.id = req.params.id;
const errors = await validate(dto);
if (errors.length > 0) {
return res.status(400).json({ error: 'Invalid ID' });
}
// Параметризованный запрос
const result = await pool.query(
'SELECT id, email, name FROM users WHERE id = $1',
[dto.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error('Database error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
Чеклист защиты
- Используешь параметризованные запросы ВСЕГда
- Никогда не конкатенируешь пользовательский ввод в SQL
- Используешь ORM или prepared statements
- Валидируешь входные данные
- Даёшь БД пользователю только нужные права
- Логируешь подозрительные запросы
- Используешь HTTPS для всех запросов
- Регулярно проверяешь логи на атаки
Вывод: SQL Injection легко предотвратить, если ВСЕГДА использовать параметризованные запросы. Это правило номер 1 для безопасности БД.