Rails CSRF Protection + Angular.js: protection_from_forgery змушує мене вийти з POST


129

Якщо protect_from_forgeryпараметр згадується у application_controller, я можу ввійти та виконувати будь-які GET-запити, але вже на перший POST-запит Rails скидає сеанс, який виходить із системи.

Я тимчасово вимкнув цю protect_from_forgeryопцію, але хотів би використовувати її з Angular.js. Чи є якийсь спосіб це зробити?


Дивіться , якщо це допомагає будь-кому, його щодо налаштування HTTP заголовків stackoverflow.com/questions/14183025 / ...
— Марк Rajcok

Відповіді:


276

Я вважаю, що читання CSRF-значення з DOM не є гарним рішенням, це лише обхідне рішення.

Ось офіційний веб-сайт angularJS http://docs.angularjs.org/api/ng.$http :

Оскільки тільки JavaScript, який працює на вашому домені, може читати файли cookie, ваш сервер може бути впевнений, що XHR прийшов з JavaScript, що працює на вашому домені.

Щоб скористатися цим (Захист CSRF), ваш сервер повинен встановити маркер у файлі cookie, що читається в JavaScript, під назвою XSRF-TOKEN при першому запиті HTTP GET. На наступних запитах, які не є GET, сервер може перевірити, що файл cookie відповідає заголовку HTTP X-XSRF-TOKEN

Ось моє рішення на основі цих інструкцій:

Спочатку встановіть файл cookie:

# app/controllers/application_controller.rb

# Turn on request forgery protection
protect_from_forgery

after_action :set_csrf_cookie

def set_csrf_cookie
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end

Потім ми повинні перевірити маркер для кожного запиту, який не GET.
Оскільки Rails вже побудований подібним методом, ми можемо просто просто його перекрити, щоб додати нашу логіку:

# app/controllers/application_controller.rb

protected
  
  # In Rails 4.2 and above
  def verified_request?
    super || valid_authenticity_token?(session, request.headers['X-XSRF-TOKEN'])
  end

  # In Rails 4.1 and below
  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

18
Мені подобається ця методика, оскільки вам не потрібно змінювати жоден код на стороні клієнта.
— Мішель Тіллі

11
Як це рішення зберігає корисність захисту CSRF? Встановивши файл cookie, браузер із зазначеним користувачем надішле це cookie на всі наступні запити, включаючи запити між веб-сайтами. Я можу створити шкідливий сторонній сайт, який надсилає зловмисний запит, і браузер користувача надсилатиме на сервер "XSRF-TOKEN". Схоже, що це рішення рівносильно повністю відключити захист CSRF.
— Стівен

9
З кутових документів: "Оскільки тільки JavaScript, який працює на вашому домені, може читати файли cookie, ваш сервер може бути впевнений, що XHR прийшов з JavaScript, що працює на вашому домені." @StevenXu - Як би сторонній сайт читав файли cookie?
— Джиммі Бейкер

8
@JimmyBaker: так, ти маєш рацію. Я переглянув документацію. Підхід концептуально обгрунтований. Я переплутав налаштування файлу cookie з валідацією, не розуміючи, що Angular Framework встановлює спеціальний заголовок на основі значення файлу cookie!
— Стівен

5
form_authenticity_token генерує нові значення при кожному виклику в Rails 4.2, тому, здається, це більше не працює.
— Дейв

78

Якщо ви використовуєте захист CSRF Rails за замовчуванням ( <%= csrf_meta_tags %>), ви можете налаштувати свій кутовий модуль так:

myAngularApp.config ["$httpProvider", ($httpProvider) ->
  $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content')
]

Або якщо ви не використовуєте CoffeeScript (що !?):

myAngularApp.config([
  "$httpProvider", function($httpProvider) {
    $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content');
  }
]);

Якщо ви хочете, ви можете надсилати заголовок лише на запити, які не є GET, чимось на зразок наступного:

myAngularApp.config ["$httpProvider", ($httpProvider) ->
  csrfToken = $('meta[name=csrf-token]').attr('content')
  $httpProvider.defaults.headers.post['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.put['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.patch['X-CSRF-Token'] = csrfToken
  $httpProvider.defaults.headers.delete['X-CSRF-Token'] = csrfToken
]

Також не забудьте переглянути відповідь HungYuHei , яка охоплює всі основи на сервері, а не клієнт.


Дозволь пояснити. Базовий документ - це звичайний HTML, а не .erb, тому я не можу використовувати <%= csrf_meta_tags %>. Я подумав, що має бути достатньо protect_from_forgeryлише згадати . Що робити? Базовим документом повинен бути звичайний HTML (я тут не той, хто обирає).
— Павло

3
Коли ви використовуєте protect_from_forgeryте, що ви говорите, "коли мій код JavaScript робить запити Ajax, я обіцяю надіслати X-CSRF-Tokenв заголовку, що відповідає поточному маркеру CSRF". Для того, щоб отримати цей маркер, Rails вводить його в DOM з <%= csrf_meta_token %>і отримує вміст метатега з jQuery кожного разу, коли він робить запити Ajax (драйвер UJS Rails 3 робить це за вас). Якщо ви не використовуєте ERB, немає можливості перенести поточний маркер з Rails на сторінку та / або JavaScript - і тому ви не можете використовувати protect_from_forgeryцей спосіб.
— Мішель Тіллі

Дякую за пояснення. Я думав, що в класичному додатку на стороні сервера клієнт отримує csrf_meta_tagsщоразу, коли сервер генерує відповідь, і кожен раз ці теги відрізняються від попередніх. Отже, ці теги є унікальними для кожного запиту. Питання: як програма отримує ці теги для запиту AJAX (без кутового)? Я використовував protection_from_forgery з POQ-запитами jQuery, ніколи не заважав отримувати цей маркер CSRF, і він працював. Як?
— Павло

1
Драйвер Rails UJS використовує, jQuery.ajaxPrefilterяк показано тут: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/… Ви можете ознайомитись з цим файлом і побачити, як усі обручі Рейки перескакують, щоб змусити його працювати майже без необхідності турбуйся про це.
— Мішель Тіллі

@BrandonTilley чи не було б сенсу робити це тільки на, putа postне на common? З рейок посібник з безпеки :The solution to this is including a security token in non-GET requests
— християнські повірки

29

The Angular_rails_csrf камінь автоматично додає підтримку для шаблону , описаного в відповідь HungYuHei по всім контролерам:

# Gemfile
gem 'angular_rails_csrf'

будь-яка ідея, як слід налаштувати контролер програми та інші налаштування, пов'язані з csrf / підробкою, щоб правильно використовувати angular_rails_csrf?
— Бен Уілер

На момент отримання цього коментаря angular_rails_csrfдорогоцінний камінь не працює з Rails 5. Однак налаштування кутових заголовків запиту зі значенням метатега CSRF працює!
— bideowego


4

Відповідь, що поєднує всі попередні відповіді, і покладається на те, що ви використовуєте Devise дорогоцінний камінь автентифікації.

Перш за все, додайте дорогоцінний камінь:

gem 'angular_rails_csrf'

Далі додайте rescue_fromблок у application_controller.rb:

protect_from_forgery with: :exception

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  render text: 'Invalid authenticity token', status: :unprocessable_entity
end

І нарешті, додайте модуль перехоплювача до кутового додатка.

# coffee script
app.factory 'csrfInterceptor', ['$q', '$injector', ($q, $injector) ->
  responseError: (rejection) ->
    if rejection.status == 422 && rejection.data == 'Invalid authenticity token'
        deferred = $q.defer()

        successCallback = (resp) ->
          deferred.resolve(resp)
        errorCallback = (resp) ->
          deferred.reject(resp)

        $http = $http || $injector.get('$http')
        $http(rejection.config).then(successCallback, errorCallback)
        return deferred.promise

    $q.reject(rejection)
]

app.config ($httpProvider) ->
  $httpProvider.interceptors.unshift('csrfInterceptor')

1
Чому ви робите ін'єкцію $injectorзамість того, щоб просто вводити безпосередньо$http ?
— whitehat101

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

1

Я побачив інші відповіді і подумав, що вони чудові і добре продумані. У мене програма для рейок працює, хоча з тим, що я вважав, є більш простим рішенням, тому я подумала, що поділюсь. Мій додаток для рейок прийшов із цим дефолтом у ньому,

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
end

Я читав коментарі, і здавалося, що саме так я хочу використовувати кутовий і уникати помилки csrf. Я змінив це на це,

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session
end

А тепер це працює! Я не бачу жодної причини, чому це не повинно працювати, але я хотів би почути деяку інформацію з інших плакатів.


6
це спричинить проблеми, якщо ви намагаєтеся використовувати сеанси рейлів ', оскільки він буде встановлений на нуль, якщо він не виконає тест підробки, що було б завжди, оскільки ви не надсилаєте csrf-маркер з боку клієнта.
— хаджпой

Але якщо ви не використовуєте Rails сесії, все добре; спасибі! Я намагаюся знайти найчистіше рішення цього.
— Морган

1

Я використовував вміст відповіді HungYuHei у своїй заяві. Я виявив, що маю справу з кількома додатковими проблемами, однак деякі з-за мого використання Devise для аутентифікації, а деякі через дефолт, який я отримав зі своєю заявою:

protect_from_forgery with: :exception

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

  protect_from_forgery with: :exception

  after_filter :set_csrf_cookie_for_ng

  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

  rescue_from ActionController::InvalidAuthenticityToken do |exception|
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
    render :error => 'Invalid authenticity token', {:status => :unprocessable_entity} 
  end

protected
  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

1

Я знайшов дуже швидкий злом до цього. Все, що мені потрібно було зробити, це:

а. На мій погляд, я ініціалізую $scopeзмінну, яка містить маркер, скажімо перед формою, а ще краще при ініціалізації контролера:

<div ng-controller="MyCtrl" ng-init="authenticity_token = '<%= form_authenticity_token %>'">

б. Перед тим, як зберегти новий запис, у своєму контролері AngularJS я додаю маркер у хеш:

$scope.addEntry = ->
    $scope.newEntry.authenticity_token = $scope.authenticity_token 
    entry = Entry.save($scope.newEntry)
    $scope.entries.push(entry)
    $scope.newEntry = {}

Більше нічого не потрібно робити.


0
 angular
  .module('corsInterceptor', ['ngCookies'])
  .factory(
    'corsInterceptor',
    function ($cookies) {
      return {
        request: function(config) {
          config.headers["X-XSRF-TOKEN"] = $cookies.get('XSRF-TOKEN');
          return config;
        }
      };
    }
  );

Це працює з боку angularjs!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.