Rails CSRF Protection + Angular.js: protect_from_forgery заставляет меня выйти из системы через POST


129

Если эта protect_from_forgeryопция упоминается в application_controller, я могу войти в систему и выполнить любые запросы GET, но при самом первом запросе POST Rails сбрасывает сеанс, в результате чего я выхожу из системы.

Я protect_from_forgeryвременно отключил эту опцию, но хотел бы использовать ее с Angular.js. Есть ли способ сделать это?


Посмотрите, поможет ли это кому-нибудь, речь
идет

Ответы:


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
Из документации Angular: «Поскольку только JavaScript, который работает в вашем домене, может читать cookie, ваш сервер может быть уверен, что XHR поступил из JavaScript, запущенного в вашем домене». @StevenXu - Как сторонний сайт будет читать cookie?
Джимми Бейкер

8
@ Джимми Бейкер: да, ты прав. Я просмотрел документацию. Подход концептуально обоснован. Я перепутал настройку cookie с проверкой, не понимая, что Angular фреймворк устанавливает настраиваемый заголовок на основе значения cookie!
Стивен

5
form_authenticity_token генерирует новые значения при каждом вызове в Rails 4.2, поэтому, похоже, это больше не работает.
Дэйв

78

Если вы используете защиту Rails CSRF по умолчанию ( <%= csrf_meta_tags %>), вы можете настроить свой модуль Angular следующим образом:

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 и получает содержимое метатега с помощью jQuery всякий раз, когда он делает запросы Ajax (драйвер Rails 3 UJS по умолчанию делает это за вас). Если вы не используете ERB, нет способа получить текущий токен из Rails на странице и / или в JavaScript - и, следовательно, вы не можете использовать его protect_from_forgeryтаким образом.
Мишель Тилли

Спасибо за объяснение. Я думал, что в классическом серверном приложении клиентская сторона получает csrf_meta_tagsкаждый раз, когда сервер генерирует ответ, и каждый раз эти теги отличаются от предыдущих. Итак, эти теги уникальны для каждого запроса. Возникает вопрос: как приложение получает эти теги для запроса AJAX (без angular)? Я использовал protect_from_forgery с запросами jQuery POST, никогда не удосужился получить этот токен CSRF, и это сработало. Как?
Пол

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

@BrandonTilley, разве не имеет смысла делать это только putи postвместо этого common? Из руководства по безопасности рельсов :The solution to this is including a security token in non-GET requests
christianvuerings

29

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

# Gemfile
gem 'angular_rails_csrf'

есть идеи, как настроить контроллер приложения и другие параметры, связанные с csrf / подделкой, чтобы правильно использовать angular_rails_csrf?
Бен Уиллер

На момент написания этого комментария angular_rails_csrfгем не работает с Rails 5. Однако настройка заголовков запросов Angular со значением из метатега 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

И, наконец, добавьте модуль перехватчика в свое приложение angular.

# 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

Я видел другие ответы и думал, что они отличные и хорошо продуманные. Я заставил мое приложение rails работать с тем, что я считал более простым решением, поэтому я решил поделиться. В моем приложении rails это было по умолчанию,

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

Я прочитал комментарии, и мне показалось, что это то, что я хочу использовать angular и избежать ошибки 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
это вызовет проблемы, если вы пытаетесь использовать rails 'sessions', поскольку для него будет установлено значение nil, если он не пройдет тест на подделку, что будет всегда, поскольку вы не отправляете csrf-токен со стороны клиента.
hajpoj

Но если вы не используете сеансы 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.