Статьи

Массовое задание, рельсы и ты

В начале 2012 года разработчик по имени Егор Хомаков воспользовался дырой в безопасности в Github (приложение Rails), чтобы получить коммитный доступ к проекту Rails .

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


Для начала давайте сначала посмотрим, что означает массовое назначение и почему оно существует. В качестве примера представим, что в нашем приложении есть следующий класс User :

1
2
3
# Assume the following fields: [:id, :first, :last, :email]
class User < ActiveRecord::Base
end

Массовое назначение позволяет нам устанавливать сразу несколько атрибутов:

1
2
3
4
5
attrs = {:first => «John», :last => «Doe», :email => «[email protected]»}
user = User.new(attrs)
user.first #=> «John»
user.last #=> «Doe»
user.email #=> «[email protected]»

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

1
2
3
4
5
6
7
8
attrs = {:first => «John», :last => «Doe», :email => «[email protected]»}
user = User.new
user.first = attrs[:first]
user.last = attrs[:last]
user.email = attrs[:email]
user.first #=> «John»
user.last #=> «Doe»
user.email #=> «[email protected]»

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


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

Но ждать! Одна проблема с острыми инструментами заключается в том, что вы можете порезаться ими. Массовое назначение не является исключением из этого правила.

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

1
2
3
4
5
class AddCanFireMissilesFlagToUsers < ActiveRecord::Migration
  def change
    add_column :users, :can_fire_missiles, :boolean, :default => false
  end
end

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

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

1
PUT http://missileapp.com/users/42?user[first]=NewJohn&user[email][email protected]

Действие update в UsersController может выглядеть примерно так:

1
2
3
4
5
6
7
8
def update
  user = User.find(params[:id])
  if user.update_attributes(params[:user]) # Mass assignment!
    redirect_to home_path
  else
    render :edit
  end
end

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

1
2
3
{:id => 42, :user => {:first => «NewJohn», :email => «[email protected]»}
# :id — parsed by the router
# :user — parsed from the incoming querystring

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

1
PUT http://missileapp.com/users/42?user[can_fire_missiles]=true

Поля, такие как :admin :public_key :owner и :public_key , довольно легко угадать.

Когда этот запрос достигает нашего действия по update , вызов update_attributes увидит {:can_fire_missiles => true} и даст NewJohn возможность запускать ракеты! Горе стало нам.

Именно так Егор Хомаков предоставил себе доступ к проекту Rails. Поскольку Rails настолько :public_key такие поля, как :admin :public_key :owner и :public_key довольно легко угадать. Кроме того, если нет средств защиты, вы можете получить доступ к вещам, которые вы не должны касаться.


Так как же нам защитить себя от бессмысленных массовых назначений? Как мы можем предотвратить неосторожный запуск «Нью-Джонс мира» наших ракет?

К счастью, Rails предоставляет несколько инструментов для решения проблемы: attr_protected и attr_accessible .

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

1
2
3
class User < ActiveRecord::Base
  attr_protected :can_fire_missiles
end

Теперь любая попытка массового присвоения атрибута can_fire_missiles потерпит неудачу.

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

Здесь и вступает attr_accessible . Как вы уже догадались, это противоположность attr_protected : перечисляйте только те атрибуты, которые вы хотите назначить массово.

Таким образом, мы можем переключить наш класс User на такой подход:

1
2
3
class User < ActiveRecord::Base
  attr_accessible :first, :last, :email
end

Здесь мы явно перечисляем то, что может быть назначено по массе. Все остальное будет запрещено. Преимущество здесь в том, что, если мы, скажем, добавим флаг admin в модель User , он автоматически будет защищен от массового назначения.

Как правило, вы должны предпочесть attr_accessible attr_protected , так как это поможет вам ошибиться в случае осторожности.

В Rails 3.1 введено понятие массового назначения «ролей». Идея состоит в том, что вы можете указать разные attr_protected и attr_accessible для разных ситуаций.

01
02
03
04
05
06
07
08
09
10
class User < ActiveRecord::Base
  attr_accessible :first, :last, :email # :default role
  attr_accessible :can_fire_missiles, :as => :admin # :admin role
end
 
user = User.new({:can_fire_missiles => true}) # uses the :default role
user.can_fire_missiles #=> false
 
user2 = User.new({:can_fire_missiles => true}, :as => :admin)
user.can_fire_missiles #=> true

Вы можете контролировать поведение массового назначения в своем приложении, отредактировав параметр config.active_record.whitelist_attributes в файле config/application.rb .

Если установлено значение false , защита массового присвоения будет активирована только для моделей, в которых вы указали список attr_protected или attr_accessible .

Если установлено значение true , массовое назначение будет невозможно для всех моделей, если они не задают список attr_protected или attr_accessible . Обратите внимание, что эта опция включена по умолчанию из Rails 3.2.3 и выше.

Начиная с Rails 3.2, дополнительно есть опция конфигурации для контроля строгости защиты массовых назначений: config.active_record.mass_assignment_sanitizer .

Если установлено значение :strict , оно будет вызывать ActiveModel::MassAssignmentSecurity::Error каждый раз, когда ваше приложение пытается массово назначить то, что не должно. Вам нужно будет обработать эти ошибки явно. Начиная с версии 3.2, этот параметр установлен для вас в средах разработки и тестирования (но не в рабочей среде), предположительно, чтобы помочь вам отследить возможные проблемы массового назначения.

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


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

Инцидент с Хомаковым положил начало разговору о защите массовых назначений в сообществе Rails (и далее на других языках); был задан интересный вопрос: относится ли защита массовых назначений к уровню модели?

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

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

Результатом этого обсуждения является гем Strong Parameters , доступный для использования с Rails 3, и по умолчанию в следующем выпуске Rails 4.

Предполагая, что наше ракетное приложение работает на Rails 3, вот как мы можем обновить его для использования с параметром stong:

Добавьте следующую строку в Gemfile:

1
gem strong_parameters

В config/application.rb :

1
config.active_record.whitelist_attributes = false
1
2
3
class User < ActiveRecord::Base
  include ActiveModel::ForbiddenAttributesProtection
end
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
class UsersController < ApplicationController
  def update
    user = User.find(params[:id])
    if user.update_attributes(user_params) # see below
      redirect_to home_path
    else
      render :edit
    end
  end
 
  private
 
  # Require that :user be a key in the params Hash,
  # and only accept :first, :last, and :email attributes
  def user_params
    params.require(:user).permit(:first, :last, :email)
  end
end

Теперь, если вы попытаетесь что-то вроде user.update_attributes(params) , вы получите ошибку в вашем приложении. Сначала вы должны вызвать permit для хэша params с ключами, которые разрешены для определенного действия.

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

Примечание. Если бы это было приложение на Rails 4, нам понадобился бы код контроллера; функциональность сильных параметров будет включена по умолчанию. В результате вам не понадобится включение в модель или отдельный гем в Gemfile.


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

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