На прошлой неделе мы рассмотрели способы, которыми злоумышленники могут пытаться похитить сессии действительных пользователей. На этой неделе мы рассмотрим две опасности, с которыми вы сталкиваетесь, когда злоумышленник регистрируется на вашем сайте. Мы еще раз посмотрим, как Rails защищает вас, и как вы можете защитить себя дальше.
Изменение неожиданных атрибутов
4 марта 2012 года была успешно использована уязвимость в Github, позволяющая злоумышленнику получить доступ к хранилищу Rails. Взлом был уязвимость массового назначения — распространенная проблема в большинстве приложений Rails, которые я видел. Хак очень прост в реализации, и есть большая вероятность того, что у вас есть эта дыра в безопасности в ваших приложениях.
Это такая большая уязвимость и такое простое исправление — почему бы не потратить некоторое время и обновить свои приложения сегодня?
Давайте посмотрим на этот хак подробно. Начнем с базовой модели, контроллера и формы:
class User < ActiveRecord::Base | |
# Has attributes: [:username, :hashed_password, :is_admin] | |
end |
class UsersController < ApplicationController | |
#… | |
def update | |
@user = User.find(params[:id]) | |
@user.update_attributes(params[:user]) | |
#… | |
end | |
#… | |
end |
<%= form_for @user do |f| %> | |
<%= f.label :username %> | |
<%= f.text_field :username %> | |
<%= submit_tag %> | |
<% end %> |
Это очень стандартный шаблон. Контроллер ожидает, что params[:user]
будет выглядеть примерно так {:username => 'iHiD'}
. Контроллер обновляет модель с новым именем пользователя и затем перенаправляет на страницу показа.
Теперь предположим, что я хитрый хакер, который хочет получить доступ администратора к этому сайту. Я {:username => 'iHiD', is_admin: true}
запрос, который отправляет данные {:username => 'iHiD', is_admin: true}
в приложение. Затем контроллер послушно обновляет @user
с помощью этого хэша, и я только что стал администратором. В случае с Github злонамеренный пользователь добавил открытый ключ в организацию Rails, предоставив им права коммитов.
Это все очень плохо.
Почему Rails не защищает меня от этого, вы кричите! Что ж, это так — он предоставляет вам все инструменты, чтобы исправить это, но вы сами должны его применять. Rails предоставляет две опции в вашей модели для защиты атрибутов: attr_protected
и attr_accessible
. Добавив attr_protected
в вашу модель, вы можете занести в черный список атрибуты, которые нельзя назначать массово. Например:
class User < ActiveRecord::Base | |
# Has attributes: [:username, :hashed_password, :is_admin] | |
attr_protected :is_admin | |
end |
Любая попытка установить is_admin
через update_attributes
теперь приведет к ошибке. Это шаг в правильном направлении, но, на мой взгляд, этого недостаточно. Элементы черного списка очень уязвимы для ошибок пользователя. Вы должны помнить, чтобы обновлять эту функцию каждый раз, когда вы создаете новый атрибут. Кроме того, существует одна малоизвестная проблема с assign_attributes
которой большинство разработчиков не знают. Эта функция не только присваивается атрибутам — методы также уязвимы для массового присваивания . Вот надуманный пример:
# Migration | |
create_table :users do |t| | |
t.boolean :can_do_dangerous_things, null: false | |
#… | |
t.timestamps | |
end | |
class User < ActiveRecord::Base | |
# Blacklisting attribute | |
attr_protected :can_do_dangerous_things | |
before_create do | |
return true if @permissions_set | |
self.permissions = { | |
:can_do_dangerous_things => false | |
#… | |
} | |
true | |
end | |
def permissions=(hash) | |
self.can_do_dangerous_things = hash[:can_do_dangerous_things] | |
#… | |
@permissions_set = true | |
end | |
end |
Этот код устанавливает для can_do_dangerous_things
значение false по умолчанию и защищает нас от прямого доступа к нему. Тем не менее, это не защищает наш вспомогательный метод!
User.create! | |
# -> #<User id: 2, can_do_dangerous_things: false, …> | |
User.create!(:can_do_dangerous_things => true) | |
# -> ActiveModel::MassAssignmentSecurity::Error: Can’t mass-assign protected attributes: can_do_dangerous_things | |
User.create!(:permissions => {:can_do_dangerous_things => true}) | |
# -> #<User id: 2, can_do_dangerous_things: true, …> |
Таким образом, хотя attr_protected
помогает, это только так далеко. Вместо этого мы должны использовать attr_accessible
для элементов белого списка. Фактически, после взлома Github, Rails выпустил 3.2.3, который изменил опцию конфигурации, чтобы заставить attr_accessible
по умолчанию. Если вы создали свое приложение до 3.2.3, то установите следующее в вашей конфигурации:
config.active_record.whitelist_attributes = true |
Это позволит только attr_accessible
к атрибутам, указанным в attr_accessible
, через массовое назначение. Теперь мы можем изменить модель пользователя на:
class User < ActiveRecord::Base | |
# Has attributes: [:username, :hashed_password, :is_admin] | |
attr_accessible :username | |
end |
Это исправляет дыру в безопасности, но добавляет проблему, если вы внутренне используете update_attributes
для установки атрибутов, которые вы не хотите, чтобы пользователи могли массово назначать. Чтобы решить эту проблему, вы можете установить роли, которые для attr_accessible
. Я обычно добавляю роль с именем :internal
которая имеет больший диапазон доступных атрибутов для пакетных обновлений или внутренней функциональности. Чтобы сделать это на нашей модели User, сделайте следующее:
class User < ActiveRecord::Base | |
# Has attributes: [:username, :hashed_password, :is_admin] | |
attr_accessible :username | |
attr_accessible :username, :is_admin, :as => :internal | |
end |
Теперь я могу сделать это:
User.update_attributes(:username => «iHiD») | |
User.update_attributes({:username => «iHiD», :is_admin => true}, :as => :internal) |
Наш код теперь безопасен, и мы не создали никаких ограничений для себя. Выиграть!
Как я сказал в начале, это такая большая уязвимость и такое простое исправление — почему бы не потратить некоторое время и обновить свои приложения сегодня?
Безопасные запросы к базе данных
SQL-инъекция является хорошо документированной темой и хорошо понятна большинству разработчиков. Rails позволяет легко избежать этого, но, если вы не будете осторожны, вы можете удалить всю эту защиту. Чтобы быстро подвести итог, злоумышленник может создать умный SQL, который передается в качестве параметра вашему приложению и выполняется злонамеренно. В качестве примера, скажем, у вас есть страница, которая позволяет пользователям осуществлять поиск по своим проектам. Пользователь, посещающий /projects?name=rai
нажимает следующий код и получает все свои проекты, начинающиеся с «rai»:
class ProjectsController < ApplicationController | |
def index | |
@projects = Project.where( | |
«user_id = #{current_user.id} AND name LIKE ‘#{params[:name]}%'» | |
) | |
#… | |
end | |
end |
Допустим, я злонамеренный пользователь, который хочет взломать ваш код, я мог бы посетить
/projects?name='%20OR%20created_at%20LIKE%20'%
, который будет выполнять следующий SQL:
SELECT * FROM «projects« | |
WHERE user_id = 1 | |
AND name LIKE ‘‘ OR created_at LIKE ‘%‘ |
Внезапно я вижу все проекты! Это плохо.
Давайте начнем с исправления этой уязвимости — это просто сделать. Rails защищает нас от внедрения SQL, если мы используем его вспомогательные методы, а не строки. Мы можем использовать три безопасных метода: хеш, заполнители и переменные связывания. Вот три примера:
# Use a hash | |
Project.where(:user_id => current_user.id) | |
# Use placeholders | |
Project.where(«user_id = ?», current_user.id) | |
# Use bind variables | |
Project.where(«user_id = :user_id», {:user_id => current_user.id}) |
Итак, давайте обновим наш поисковый код. Для первой части с user_id мы можем использовать хеш. Однако мы используем LIKE для второй части, поэтому мы не можем использовать хеш, и переменные связывания кажутся ненужными, если мы используем только одну переменную, поэтому давайте использовать заполнитель. Вот наш обновленный код:
@projects = Project.where(:user_id => current_user.id). | |
where(‘name LIKE ?’, «#{params[:name]}%») |
Тем не менее, мы можем сделать еще один шаг вперед. Поскольку у project
есть user_id
, в нашей модели User
у нас будет has_many :projects
. Мы можем воспользоваться этим, используя ассоциацию в нашем запросе и удалив одно из условий where
. Давайте обновим наш код:
class ProjectsController < ApplicationController | |
def index | |
@project = current_user.projects.where(‘name LIKE ?’, «#{params[:name]}%») | |
#… | |
end | |
end |
Теперь мы защитили наш код от внедрения SQL-кода, сделали его более читабельным и более понятным. Если вы когда-нибудь видели SQL с интерполяцией в своем коде Rails, считайте, что это огромный запах кода, и немедленно обращайтесь к нему.
Дальнейшее чтение
В этой статье объясняются две критически важные уязвимости и способы защиты от них. Тем не менее, при разработке приложений на Rails я должен знать о многих других вещах, о которых я здесь не рассказывал, поэтому обязательно прочитайте Руководство по безопасности Rails, чтобы узнать больше.
На следующей неделе, в заключительной части серии, мы рассмотрим, как вы защищаете представления, которые вы отображаете, и рассмотрим несколько других общих вопросов, о которых вам следует знать.