SQL-инъекции — одна из самых опасных уязвимостей веб-приложений, способная превратить безобидный параметр запроса в полноценную атаку на базу данных. В Rails эта проблема часто маскируется за удобством Active Record, создавая ложное ощущение безопасности. Разберёмся, где скрываются риски и как их устранить.
🔍 Как работает SQL-инъекция в Rails?
Представьте: у вас есть поиск пользователей по имени:
User.where("name = '#{params[:name]}'")
Кажется безобидным? А теперь передадим в name значение ' OR '1'='1:
SELECT * FROM users WHERE name = '' OR '1'='1'
Бабах! Мы получили всех пользователей системы. Это классическая SQL-инъекция.
Почему Rails не защищает автоматически?
Active Record экранирует параметры только при использовании плейсхолдеров. Но разработчики часто:
- Пишут “сырые” SQL-строки
- Динамически собирают запросы из параметров
- Используют
sanitize_sqlнеправильно
💀 Антипаттерны: как ломают ваши приложения
1. Конкатенация строк в запросах
# Плохо (уязвимо)
User.where("email LIKE '%#{params[:search]}%'")
# Хорошо (безопасно)
User.where("email LIKE ?", "%#{params[:search]}%")
2. Динамический order из параметров
# Опасный код в контроллере
@users = User.order(params[:sort] + " " + params[:direction])
# Эксплойт:
# ?sort=id;DROP TABLE users;&direction=--
3. Прямое использование params в execute
# Катастрофа!
ActiveRecord::Base.connection.execute("SELECT * FROM users WHERE id = #{params[:id]}")
🛡️ Защитные механизмы Rails
1. Плейсхолдеры
# Безопасные варианты:
User.where("name = ?", params[:name])
User.where(name: params[:name])
User.where("name = :name", name: params[:name])
2. sanitize_sql для сложных случаев
safe_condition = ActiveRecord::Base.sanitize_sql(["role = ?", params[:role]])
User.where(safe_condition)
3. Whitelisting для сортировки
# В контроллере
def sort_column
%w[name email created_at].include?(params[:sort]) ? params[:sort] : "name"
end
def sort_direction
%w[asc desc].include?(params[:direction]) ? params[:direction] : "asc"
end
# В запросе
@users = User.order("#{sort_column} #{sort_direction}")
� Реальный кейс: как мы ловили инъекцию в продакшене
Симптомы: Необъяснимые 500-е ошибки, странные логи в PostgreSQL.
Расследование: В логах обнаружились запросы вида:
SELECT * FROM transactions WHERE amount > 0 AND (SELECT 1 FROM pg_sleep(10)) --
Виновник:
Transaction.where("amount > #{params[:min_amount]") if params[:min_amount].present?
Решение:
- Срочный хотфикс с
to_f:Transaction.where("amount > ?", params[:min_amount].to_f) - Добавили RSpec-тест:
it "ignores SQL injection in min_amount" do get :index, params: { min_amount: "0; DROP TABLE transactions;" } expect(response).to be_successful end
🧪 Тестирование на уязвимости
1. Руби-гем brakeman
Добавьте в Gemfile:
group :development do
gem 'brakeman', require: false
end
Запуск:
brakeman -q -w2
2. RSpec-тесты с вредоносными параметрами
describe "SQL injection protection" do
it "sanitizes user input in where clauses" do
expect {
User.where("name = '#{'\' OR \'1\'=\'1'}'").to_a
}.to raise_error(ActiveRecord::StatementInvalid)
end
end
🚀 Продвинутая защита: Arel и хранимые процедуры
Для сложных запросов используйте Arel — SQL-билдер Rails:
users = User.arel_table
query = users[:name].eq(params[:name]).and(users[:active].eq(true))
User.where(query)
Для критических операций — хранимые процедуры PostgreSQL:
ActiveRecord::Base.connection.execute(
"SELECT secure_user_search(#{ActiveRecord::Base.sanitize_sql(params[:query])})"
)
📝 Вывод: правила параноика
- Никогда не интерполируйте
paramsнапрямую в SQL - Для динамического SQL используйте только:
- Плейсхолдеры (
?) - Именованные параметры (
:name) sanitize_sql
- Плейсхолдеры (
- Тестируйте все endpoints с специально сформированными параметрами
- Регулярно запускайте brakeman
- Для админок и API используйте strong parameters + whitelisting
“Доверяй, но проверяй” — хороший принцип для жизни, но ужасный для работы с
paramsв Rails. Здесь работает только “Не доверяй вообще”.
# Ваш код после прочтения статьи:
User.where("trust_level > ?", params[:trust].to_i)
# Спасибо, что не доверяете params как себе 😉