Статьи

Методы защиты вашего сайта с помощью Ruby On Rails (часть 2)

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

Изменение неожиданных атрибутов

4 марта 2012 года была успешно использована уязвимость в Github, позволяющая злоумышленнику получить доступ к хранилищу Rails. Взлом был уязвимость массового назначения — распространенная проблема в большинстве приложений Rails, которые я видел. Хак очень прост в реализации, и есть большая вероятность того, что у вас есть эта дыра в безопасности в ваших приложениях.

Это такая большая уязвимость и такое простое исправление — почему бы не потратить некоторое время и обновить свои приложения сегодня?

Давайте посмотрим на этот хак подробно. Начнем с базовой модели, контроллера и формы:

class User < ActiveRecord::Base
# Has attributes: [:username, :hashed_password, :is_admin]
end

view raw
user.rb
hosted with ❤ by GitHub

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 %>

view raw
edit.html.erb
hosted with ❤ by GitHub

Это очень стандартный шаблон. Контроллер ожидает, что 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

view raw
user.rb
hosted with ❤ by GitHub

Любая попытка установить 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

view raw
user.rb
hosted with ❤ by GitHub

Этот код устанавливает для 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

view raw
application.rb
hosted with ❤ by GitHub

Это позволит только attr_accessible к атрибутам, указанным в attr_accessible , через массовое назначение. Теперь мы можем изменить модель пользователя на:

class User < ActiveRecord::Base
# Has attributes: [:username, :hashed_password, :is_admin]
attr_accessible :username
end

view raw
User.rb
hosted with ❤ by GitHub

Это исправляет дыру в безопасности, но добавляет проблему, если вы внутренне используете 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

view raw
User.rb
hosted with ❤ by GitHub

Теперь я могу сделать это:

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 %

view raw
vulnerable.sql
hosted with ❤ by GitHub

Внезапно я вижу все проекты! Это плохо.

Давайте начнем с исправления этой уязвимости — это просто сделать. 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]})

view raw
safe_sql.rb
hosted with ❤ by GitHub

Тем не менее, мы можем сделать еще один шаг вперед. Поскольку у 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, чтобы узнать больше.

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