Если запрос собирается конкатенацией:
"SELECT * FROM users WHERE login = '" + login + "' AND password = '" + pass + "'"
то ввод admin' -- превращает остаток в комментарий, а x' OR '1'='1 делает условие истинным. Дальше хуже: UNION SELECT вытаскивает данные из других таблиц, в некоторых СУБД можно выполнить несколько команд (; DROP TABLE), прочитать файлы или выполнить команды ОС. Даже без видимого вывода работает «слепая» инъекция — по времени ответа (pg_sleep) или по разнице в поведении.
Главная защита — параметризованные запросы (prepared statements):
SELECT * FROM users WHERE login = ? AND password_hash = ?
Текст запроса и значения уходят в базу раздельно; значение никогда не разбирается как SQL, сколько бы кавычек в нём ни было. Поддержано везде: PreparedStatement в JDBC, плейсхолдеры в psycopg и SQLAlchemy, именованные параметры в JPA.
ORM использует параметры, но уязвимости возвращаются через сырой SQL: createNativeQuery("... " + filter), text(f"... {x}"), конкатенация в JPQL, extra() и raw() в Django.
Что нельзя передать параметром: имена таблиц и колонок, направление сортировки, ключевые слова. Для ORDER BY по выбору пользователя — белый список: сопоставить ввод с допустимыми колонками и подставить только известное значение. Для IN (...) — сгенерировать нужное число плейсхолдеров.
Дополнительные слои:
- пользователь базы с минимальными правами (приложению не нужен
DROP и доступ к чужим схемам); - не показывать текст ошибок СУБД пользователю;
- валидация формата (id — число) — полезна, но не замена параметрам;
- WAF как страховка, не как основная защита.
Тот же класс ошибок — любые инъекции в интерпретаторы: команды ОС (subprocess с shell=True), LDAP, NoSQL (операторы $ne в MongoDB из JSON), шаблоны, а теперь и промпты языковых моделей.