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.