Экранирование спецсимволов: защита от SQL-инъекций
Как правильно экранировать спецсимволы в строках для предотвращения SQL-инъекций и ошибок в коде
SQL-инъекция входит в топ-10 уязвимостей веб-приложений по версии OWASP и может привести к утечке базы данных или полной компрометации системы. Корень проблемы — неправильная обработка пользовательского ввода при формировании SQL-запросов. Разберём, как экранировать спецсимволы и защититься от инъекций в разных языках программирования.
Почему обычная конкатенация опасна
Классический пример уязвимого кода на PHP:
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
mysqli_query($conn, $query);
Если пользователь введёт admin' OR '1'='1, запрос превратится в:
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
Условие '1'='1' всегда истинно, поэтому запрос вернёт всех пользователей. Злоумышленник может пойти дальше и использовать admin'; DROP TABLE users; --, что удалит таблицу целиком.
Аналогичная проблема существует в любом языке, где строки запросов собираются вручную. JavaScript с Node.js, Python, Java — все уязвимы при наивном подходе.
Подготовленные запросы — золотой стандарт
Prepared statements отделяют логику запроса от данных. База получает сначала шаблон с плейсхолдерами, затем параметры отдельно:
// Node.js с mysql2
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);
# Python с psycopg2
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
)
// PHP с PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
База данных сама обрабатывает параметры, экранируя все спецсимволы. Даже если передать admin' OR '1'='1, это будет воспринято как обычная строка для поиска, а не часть SQL-кода.
Экранирование для динамических запросов
Иногда подготовленные запросы не подходят — например, когда нужно динамически менять имена таблиц или колонок. Для таких случаев используйте функции экранирования из драйвера БД:
// MySQL
$safe_table = mysqli_real_escape_string($conn, $table_name);
$query = "SELECT * FROM `$safe_table` WHERE id = ?";
# PostgreSQL
from psycopg2.extensions import quote_ident
safe_column = quote_ident(column_name, cursor)
query = f"SELECT {safe_column} FROM users WHERE id = %s"
Важно: экранирование строковых значений через mysqli_real_escape_string или аналоги — это запасной вариант. Для данных всегда используйте параметризованные запросы.
Контекст имеет значение
Экранирование зависит от того, где используется строка:
В SQL-запросах: используйте плейсхолдеры или функции драйвера (mysql_real_escape_string, pg_escape_string).
В JSON: встроенные методы JSON.stringify() в JavaScript или json.dumps() в Python автоматически экранируют кавычки и управляющие символы:
const user = { name: 'O\'Reilly', quote: 'He said: "Hi"' };
const json = JSON.stringify(user);
// {"name":"O'Reilly","quote":"He said: \"Hi\""}
В HTML: экранируйте <, >, &, ", ' чтобы предотвратить XSS:
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
В командной строке: используйте библиотеки вроде shlex в Python или избегайте eval() и exec() вообще.
Специфика разных СУБД
MySQL требует экранирования обратного слеша \, одинарной кавычки ', двойной кавычки ", нулевого байта \0, символа новой строки \n, возврата каретки \r и Ctrl+Z.
PostgreSQL строже к типам данных и различает одинарные кавычки (для строк) и двойные (для идентификаторов). Для экранирования одинарной кавычки удваивайте её: O''Reilly.
SQLite использует те же правила, что PostgreSQL, но менее требователен к типам.
MS SQL Server поддерживает квадратные скобки для идентификаторов и N-префикс для Unicode-строк: N'O''Brien'.
ORM и query builders — не панацея
Sequelize, TypeORM, SQLAlchemy и другие ORM генерируют безопасные запросы в большинстве случаев:
// Sequelize
await User.findAll({
where: { username: userInput }
});
Но как только вы используете raw-запросы, ответственность снова на вас:
// ОПАСНО!
await sequelize.query(
`SELECT * FROM users WHERE role = '${role}'`
);
// БЕЗОПАСНО
await sequelize.query(
'SELECT * FROM users WHERE role = ?',
{ replacements: [role] }
);
Query builders вроде Knex.js позволяют строить сложные запросы программно, автоматически параметризуя значения:
knex('users')
.where('status', status)
.andWhere('age', '>', age)
.select('*');
Практические советы и инструменты
Валидируйте входные данные на уровне приложения. Если ожидаете число — проверьте, что это действительно число. Для ID используйте parseInt() или Number().
Применяйте принцип минимальных привилегий: учётная запись БД должна иметь только необходимые права. Если приложению нужен только SELECT, не давайте ей DROP или DELETE.
Используйте WAF (Web Application Firewall) как дополнительный уровень защиты, но не основной.
Регулярно проверяйте код статическими анализаторами: ESLint с плагинами безопасности для JavaScript, Bandit для Python, PHPStan для PHP.
Для быстрой работы со строками и проверки экранирования пригодятся онлайн-инструменты. Кодирование URL поможет правильно передать параметры в адресной строке, Base64-кодирование — безопасно передать бинарные данные через текстовые протоколы, а MD5-хеширование — проверить целостность данных (хотя для паролей используйте bcrypt или Argon2).
Экранирование спецсимволов — базовый навык любого разработчика. Используйте подготовленные запросы по умолчанию, понимайте контекст использования строк и помните: безопасность — это слои защиты, а не одна серебряная пуля.