Сатья по безопасности Ruby on Rails (Ruby on Rails Cheat Sheet)
Введение
Статья даёт краткие базовые советы по безопасности Ruby on Rails. Она дополняет и усиливает материал официального руководства по безопасности Rails.
Rails снимает с разработчика рутину и ускоряет сложные задачи. Новичкам, не знакомым с внутренностями фреймворка, нужен минимальный набор правил — для этого и предназначен документ.
Разделы
Инъекция команд
В Ruby есть eval для динамического кода из строк и множество способов вызвать системные команды.
eval("ruby code here")
system("os command here")
`ls -al /` # (обратные кавычки — команда ОС)
exec("os command here")
spawn("os command here")
open("| os command here")
Process.exec("os command here")
Process.spawn("os command here")
IO.binread("| os command here")
IO.binwrite("| os command here", "foo")
IO.foreach("| os command here") {}
IO.popen("os command here")
IO.read("| os command here")
IO.readlines("| os command here")
IO.write("| os command here", "foo")Мощность этих вызовов высока — в веб-приложении ими лучше не пользоваться. Если без выхода, нужен строгий allow-list возможных значений и максимальная валидация ввода.
SQL-инъекции
Часто используется ActiveRecord; простые запросы часто защищены по умолчанию, но небезопасный код возможен.
name = params[:name]
@projects = Project.where("name like '" + name + "'");Уязвимость из‑за неэкранированного name.
Идиоматичный вариант:
@projects = Project.where("name like ?", "%#{ActiveRecord::Base.sanitize_sql_like(params[:name])}%")Не стройте SQL из недоверенного ввода. Примеры: rails-sqli.org. Общий контекст: SQL Injection.
Межсайтовый скриптинг (XSS)
По умолчанию строки в представлениях экранируются. Обход защиты часто делают ради rich text. Небезопасно в .erb:
# Неправильно! Так не делайте!
<%= raw @product.name %>
# Неправильно! (вывод без экранирования)
<%== @product.name %>
# Неправильно!
<%= @product.name.html_safe %>Любое поле с raw, html_safe и похожим — потенциальная цель XSS. Имя html_safe вводит в заблуждение: метод означает «мы уверены, что строка безопасна для вставки в HTML без экранирования». Сам по себе вызов небезопасен.
Разбор SafeBuffer. Другие хелперы, меняющие подготовку строк к выводу, могут давать похожие проблемы.
Если нужен HTML от пользователей — лучше лёгкий язык разметки (Markdown, Textile) без произвольных HTML-тегов.
Если HTML неизбежен — CSP без выполнения JS и метод #sanitize с белым списком тегов (sanitize исторически находили уязвимости; это не панацея).
В старых версиях Rails вектором XSS может быть href ссылки:
<%= link_to "Personal Website", @user.website %>Если @user.website начинается с javascript:, при клике выполнится скрипт:
<a href="javascript:alert('Haxored')">Personal Website</a>В новых Rails такие ссылки экранируются лучше.
link_to "Personal Website", 'javascript:alert(1);'.html_safe()
# Сгенерирует ссылку с javascript: в href — небезопасноContent Security Policy дополнительно ограничивает javascript: в ссылках.
Brakeman помогает находить XSS в Rails-приложениях.
Общий материал OWASP: Cross-site Scripting (XSS).
Сессии
По умолчанию Rails хранит сессию в cookie: без дополнительных настроек она не «истекает» на сервере — возможны replay; в сессию нельзя класть чувствительные данные.
Лучше хранилище сессий в БД — в Rails настраивается просто:
Project::Application.config.session_store :active_record_storeСм. статью по управлению сессиями.
Аутентификация
Включите TLS в продакшене:
# config/environments/production.rb
# Принудительный SSL, HSTS, secure cookies
config.force_ssl = trueРаскомментируйте/включите эту строку.
Сам Rails аутентификацию «из коробки» не задаёт; обычно используют Devise, AuthLogic и т.п.
Devise — установка:
gem 'devise'rails generate devise:installОграничьте маршруты, требующие входа:
Rails.application.routes.draw do
authenticate :user do
resources :something do # только для аутентифицированных
...
end
end
devise_for :users # регистрация/вход/выход
root to: 'static#home' # без аутентификации
endСложность пароля — devise_zxcvbn:
class User < ApplicationRecord
devise :database_authenticatable,
# другие модули devise, затем
:zxcvbnable
end# config/initializers/devise.rb
Devise.setup do |config|
config.min_password_score = 4 # порог zxcvbn
...
endДемо: PoC.
omniauth — несколько стратегий (Facebook, LDAP и др.). Интеграция.
Аутентификация по токену
Devise по умолчанию использует cookie.
Для токенов можно взять devise_token_auth (в т.ч. для SPA).
Конфигурация похожа на Devise; нужен omniauth.
# аутентификация по токену
gem 'devise_token_auth'
gem 'omniauth'mount_devise_token_auth_for 'User', at: 'auth'Генерация:
rails g devise_token_auth:install [USER_CLASS] [MOUNT_PATH]При необходимости отредактируйте миграцию под свою схему.
Если используется только токен, CSRF для этих путей не нужен. Если одновременно cookie и токен — пути с cookie по-прежнему нужно защищать от CSRF.
См. Authentication Cheat Sheet.
Небезопасные прямые ссылки на объекты (IDOR) и принудительный обход
RESTful-маршруты Rails интуитивны и угадываемы. Чтобы пользователь не читал/не менял чужие данные, проверяйте авторизацию в контроллере или через библиотеки вроде cancancan или pundit — каждая операция над объектом должна соответствовать бизнес-правилам.
Общий контекст: OWASP Top 10 (A4).
CSRF (межсайтовая подделка запроса)
В ApplicationController должно быть:
class ApplicationController < ActionController::Base
protect_from_forgery
endИсключения (except:) допустимы для API и т.п., но должны быть осознанными. Пример: для show CSRF отключён — проверьте, что это оправдано.
class ProjectController < ApplicationController
protect_from_forgery except: :show
endПо умолчанию Rails не защищает HTTP GET от CSRF.
Примечание: при одной только токен-аутентификации protect_from_forgery на этих действиях не нужен; при cookie-аутентификации защита обязательна.
Редиректы и форварды
Динамический редирект по URL из параметра опасен фишингом: доверенный домен ведёт на badhacker.com.
Пример: http://www.example.com/redirect?url=http://badhacker.com
Базовая мера — :only_path (только путь, без хоста), но опция должна входить в первый аргумент, если он хеш:
begin
if path = URI.parse(params[:url]).path
redirect_to path
end
rescue URI::InvalidURIError
redirect_to '/'
endПри проверке хоста по regexp используйте URI.parse, проверяйте .scheme и .port, якоря \A/\z, не ^/$, или жёсткий allow-list.
require 'uri'
host = URI.parse("#{params[:url]}").host
# javascript://trusted.com/%0Aalert(0) — проверяйте scheme и port
validation_routine(host) if host
def validation_routine(host)
# Якоря \A и \z, не ^ и $
# или сравнение с allow-list
endСлепой redirect_to params[:to] может привести к XSS через подстановку в параметры ответа.
Пример URL: http://example.com/redirect?to[status]=200&to[protocol]=javascript:alert(0)//
Надёжнее — карта ключ → URL:
ACCEPTABLE_URLS = {
'our_app_1' => "https://www.example_commerce_site.com/checkout",
'our_app_2' => "https://www.example_user_site.com/change_settings"
}Запрос: http://www.example.com/redirect?url=our_app_1
def redirect
url = ACCEPTABLE_URLS["#{params[:url]}"]
redirect_to url if url
endСм. Unvalidated Redirects and Forwards Cheat Sheet.
Динамические пути render
Если имя шаблона или partial строится из ввода пользователя, возможен выбор произвольного представления (в т.ч. админского). По возможности не используйте пользовательский ввод в имени/пути к view.
CORS (Cross-Origin Resource Sharing)
Для запросов с другого origin браузер требует явного разрешения. Нестандартные заголовки: ответ на OPTIONS и на основной запрос должен содержать корректный Access-Control-Allow-Origin (сначала preflight OPTIONS, затем например POST).
При обычных HTTP-запросах браузер после ответа проверяет заголовки, чтобы решить, обрабатывать ли ответ.
Allow-list в Rails с rack-cors:
Gemfile:
gem 'rack-cors', :require => 'rack/cors'config/application.rb:
module Sample
class Application < Rails::Application
config.middleware.use Rack::Cors do
allow do
origins 'someserver.example.com'
resource %r{/users/\d+.json},
:headers => ['Origin', 'Accept', 'Content-Type'],
:methods => [:post, :get]
end
end
end
endЗаголовки безопасности
В контроллере (часто в before/after_action):
response.headers['X-header-name'] = 'value'default_headers задаёт значения по умолчанию:
ActionDispatch::Response.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '0'
}HSTS задаётся в окружении, например production.rb:
config.force_ssl = trueГем secureheaders упрощает CSP и другие заголовки с учётом user-agent.
Ошибки бизнес-логики
Логические баги дают уязвимости, которые сложно ловить сканерами. Снижение риска: ревью кода, парное программирование, модульные тесты.
Поверхность атаки
Файл config/routes.rb задаёт, какие URL обрабатываются и какими контроллерами — хорошая точка для обзора поверхности.
Плохой пример (не делайте так):
# пример того, чего НЕ надо делать
match ':controller(/:action(/:id(.:format)))'Так можно вызвать любой публичный метод любого контроллера как action. Ограничивайте доступные действия явно.
Чувствительные файлы
Многие Rails-приложения в публичных репозиториях. Не коммитьте или тщательно контролируйте:
/config/database.yml — продакшен-учётные данные
/config/initializers/secret_token.rb — секрет для подписи cookie сессии
/db/seeds.rb — сиды, в т.ч. bootstrap-админ
/db/development.sqlite3 — может содержать реальные данныеШифрование
Rails опирается на криптографию ОС. Свою криптографию писать не стоит.
Devise по умолчанию хеширует пароли bcrypt — разумный выбор.
Число растяжек bcrypt в продакшене часто задаётся в /config/initializers/devise.rb:
config.stretches = Rails.env.test? ? 1 : 10Обновление Rails и зависимостей
После серии критичных CVE в 2013 году отставание от актуальных версий усложняло обновления. Большинство gems не подписаны авторами — нельзя собрать проект только из «доверенных» подписанных библиотек; имеет смысл аудит зависимостей.
Выработайте процесс обновлений, например:
- раз в месяц/квартал — плановое обновление зависимостей;
- раз в неделю — разбор важных security-адvisory;
- в исключительных случаях — срочные патчи.
Инструменты
Brakeman — статический анализ Rails; изучите типы предупреждений в документации.
Bearer — анализ Ruby и JS/TS на широкий спектр проблем из OWASP Top 10, интеграция в CI/CD.
Сканирование зависимостей: GitHub, GitLab.
Gauntlt — security-тесты на cucumber/gherkin.
dawnscanner (с 2013, по духу близок Brakeman) — статический анализ для Rails, Sinatra, Padrino; в версии 1.6.6 — сотни проверок по CVE для Ruby.
Статьи и ссылки
© Перевод на русский язык. Оригинальные материалы: OWASP Cheat Sheet Series.
Этот проект использует материалы OWASP, распространяемые по лицензии CC BY-SA 4.0.