Поради щодо безпеки у Rails застосунках

4 хв. читання

З розвитком вашого застосунку збільшується, відповідно, і кількість помилок. Найбільш проблематичні серед них — баги безпеки застосунку.

У статті розглянемо декілька рекомендацій для уникнення поширених вразливостей Rails застосунків.

Використовуйте i18n з html-тегами коректно

Досить звичною ситуацією є змішування ключів перекладу i18n з HTML тегами. Рекомендується уникати такого підходу, однак не завжди це можливо.

Розглянемо наступний приклад:

# en.yml
en:
  hello: "Welcome <strong>%{user_name}</strong>!"
<%= t('hello', user_name: current_user.first_name) %>

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

Welcome <strong>John</strong>!

Ой! Дійсно, наш рядок ніколи не був позначений як безпечний html, тому Rails уникне будь-яких проявів html.

(Поганим) розв'язанням проблеми буде застосувати наступний код:

<% # Не робіть цього! %>
<%= t('hello', user_name: current_user.first_name).html_safe %>

Хоча такий спосіб працює, нас спіткає неприємний XSS. І справді, користувач наразі може змінити своє ім'я за допомогою шкідливого JavaScript, і цей JavaScript код виконається.

XSS часто недооцінюють, вважаючи несерйозним. Однак, при правильному застосуванні, наслідки XSS можуть бути фатальними.

Рекомендоване рішення

На щастя, Rails чудово розв'язує проблему: якщо ключ i18n закінчується на _html, його буде автоматично відмічено як html-безпечний.У той же час, уникнемо інтерполяцію ключів!

# en.yml
en:
  hello_html: "Welcome <strong>%{user_name}</strong>!"
<% # Робіть так! %>
<%= t('hello_html', user_name: current_user.first_name) %>

🎉

Зауважте: наведене рішення дуже нагадує:

<% # Так не робіть також! %>
<%= t('hello', user_name: h(current_user.first_name)).html_safe %>

Надійний спосіб попередити XSS — намагатися уникати html_safe (або сирого html), якщо це можливо. В іншому випадку, двічі перевірте чи маєте ви повний контроль над відображеним контентом.

Захищайтесь за замовчуванням

Ви не можете довіряти налаштуванням користувача, і ви, швидше за все, це вже знаєте. Але існують різні способи очищення параметрів користувача.

Уявімо, що у нас є форма, і ми хочемо використовувати один з двох різних Form Objects, залежно від параметра:

class FooForm; end
class BarForm; end

form_klass = "#{params[:kind].camelize}Form".constantize # Не робіть так
form_klass.new.submit(params)

Тут ми отримуємо хороший клас форми шляхом виконання constantize для рядка, що контролюється користувачем. Такий спосіб не дуже надійний і може призвести до неприємних побічних ефектів (уявіть надсилання make_user_admin замість foo або bar).

Розв'яжемо проблему таким чином:

if params[:kind] == 'foo' || params[:kind] == 'bar'
  form_klass = "#{params[:kind].camelize}Form".constantize # Досі не робіть цього
  form_klass.new.submit(params)
end

Тут усе під контролем. Ми перевіряємо чи відповідають параметри очікуваним значенням та виконуємо constantize за необхідністю. Хоч код і працює добре, ми не виправили проблему безпеки root (що використовує constantize для користувацького вводу).

Код збільшується і розвивається, розробники постійно копіюють та вставляють окремі його частини, і на певному етапі ваш вразливий рядок опиниться в небезпеці.

Розглянемо альтернативне рішення:

klasses = {
  'foo' => FooForm,
  'bar' => BarForm
}

klass = klasses[params[:kind]]
if klass
  klass.new.submit(params)
end

Наведений фрагмент має таку ж поведінку, як у попередніх прикладах, за винятком використання constantize.

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

Остерігайтеся масивів або хешів

Розглянемо наступний код:

  # POST /delete_user?id=xxx

  def can_delete?(user_id)
    other_user = User.find_by(id: user_id)
    current_user.can_delete?(other_user)
  end

  user_id = params[:id]

  if can_delete?(user_id)
    User.where(id: user_id).update(deleted: true)
  end

Наведений код має занадто дивну структуру аби бути вразливим. Але, повірте, я бачив багато :)

Тут усе добре працює, доки не чіпати параметри.

Уявіть, що ми надсилаємо наступний запит:

POST /delete_user?id[]=42&id[]=43&id[]=44&id[]=45..

Rails парситиме параметр [:id] як масив [42, 43, 44, 45].

  User.find_by(id: [42, 43, 44, 45]) # Поверне користувача з id 42 (нижчий id)

  User.where(id: [42, 43, 44, 45]).update(deleted: true) # Оновить усе записане!

Завдяки деяким дивним речам у find_by та метушнею з параметрами, нам вдалося вплинути на записи, до яких ми можемо не мати доступу.

Завжди корисно пам'ятати, що параметри також можуть бути масивами (або хешами!), а це представляє деякі ризики з огляду безпеки.

Пам'ятайте, що злочинний інпут не завжди там, де ми його очікуємо

Більшість з нас дуже обережні при роботі з параметрами або значеннями, що надходять з бази даних.

Однак, є деякі вразливі місця, про які ми забуваємо, включаючи (але не обмежуючись) наступним:

  • Cookie: вони на 100% редагуються користувачем;
  • Інші заголовки в цілому: Referer, User-Agent тощо;
  • Користувацький IP: легко підробляється на некоректно налаштованих застосунках (використовуючи X-Forwarded-For);
  • Локальні файли: за посиланням знаходиться цікавий приклад зараження файлу ssh auth.log з метою демонстрації віддаленого виконання коду.

Завжди важливо слідкувати за справжністю та безпекою вводу, аби уникнути підробки та неприємних наслідків.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.9K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація