З розвитком вашого застосунку збільшується, відповідно, і кількість помилок. Найбільш проблематичні серед них — баги безпеки застосунку.
У статті розглянемо декілька рекомендацій для уникнення поширених вразливостей 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 з метою демонстрації віддаленого виконання коду.
Завжди важливо слідкувати за справжністю та безпекою вводу, аби уникнути підробки та неприємних наслідків.
Ще немає коментарів