Rails CSRF Protection + Angular.js: protect_from_forgery zmusza mnie do wylogowania się z POST


129

Jeśli protect_from_forgeryopcja jest wymieniona w application_controller, mogę się zalogować i wykonać dowolne żądanie GET, ale przy pierwszym żądaniu POST Railsy resetują sesję, co powoduje wylogowanie.

protect_from_forgeryTymczasowo wyłączyłem tę opcję, ale chciałbym jej używać z Angular.js. Czy jest jakiś sposób, aby to zrobić?


Zobacz, czy to w ogóle pomaga, chodzi o ustawienie nagłówków HTTP stackoverflow.com/questions/14183025/ ...
— Mark Rajcok

Odpowiedzi:


276

Myślę, że odczytywanie wartości CSRF z DOM nie jest dobrym rozwiązaniem, to tylko obejście.

Oto dokument w formie oficjalnej strony angularJS http://docs.angularjs.org/api/ng.$http :

Ponieważ tylko JavaScript działający w Twojej domenie może odczytać plik cookie, Twój serwer może mieć pewność, że XHR pochodzi z JavaScript uruchomionego w Twojej domenie.

Aby skorzystać z tego (CSRF Protection), Twój serwer musi ustawić token w pliku cookie sesji czytelnym dla JavaScript o nazwie XSRF-TOKEN przy pierwszym żądaniu HTTP GET. Przy kolejnych żądaniach innych niż GET serwer może sprawdzić, czy plik cookie pasuje do nagłówka HTTP X-XSRF-TOKEN

Oto moje rozwiązanie oparte na tych instrukcjach:

Najpierw ustaw plik 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

Następnie powinniśmy zweryfikować token przy każdym żądaniu innym niż GET.
Ponieważ Railsy zbudowały już podobną metodę, możemy po prostu nadpisać ją i dołączyć naszą logikę:

# 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
Podoba mi się ta technika, ponieważ nie musisz modyfikować żadnego kodu po stronie klienta.
— Michelle Tilley,

11
W jaki sposób to rozwiązanie zachowuje użyteczność ochrony CSRF? Ustawiając plik cookie, oznaczona przeglądarka użytkownika wyśle ​​ten plik cookie przy wszystkich kolejnych żądaniach, w tym żądaniach między witrynami. Mógłbym skonfigurować złośliwą witrynę strony trzeciej, która wysyła złośliwe żądanie, a przeglądarka użytkownika wysyłałaby „XSRF-TOKEN” do serwera. Wydaje się, że takie rozwiązanie jest równoznaczne z całkowitym wyłączeniem ochrony przed CSRF.
— Steven

9
Z dokumentacji Angular: „Ponieważ tylko JavaScript działający w Twojej domenie może odczytać plik cookie, Twój serwer może mieć pewność, że XHR pochodzi z JavaScript uruchomionego w Twojej domenie”. @StevenXu - W jaki sposób witryna strony trzeciej odczyta plik cookie?
— Jimmy Baker

8
@JimmyBaker: tak, masz rację. Przejrzałem dokumentację. Podejście jest solidne koncepcyjnie. Pomyliłem ustawienie pliku cookie z walidacją, nie zdając sobie sprawy, że platforma Angular ustawia niestandardowy nagłówek na podstawie wartości pliku cookie!
— Steven

5
form_authenticity_token generuje nowe wartości przy każdym wywołaniu w Railsach 4.2, więc wydaje się, że to już nie działa.
— Dave

78

Jeśli używasz domyślnej ochrony Rails CSRF ( <%= csrf_meta_tags %>), możesz skonfigurować swój moduł Angular w następujący sposób:

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

Lub, jeśli nie używasz CoffeeScript (co !?):

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

Jeśli wolisz, możesz wysłać nagłówek tylko w przypadku żądań innych niż GET z czymś takim jak:

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
]

Pamiętaj również, aby sprawdzić odpowiedź HungYuHei , która obejmuje wszystkie bazy na serwerze, a nie na kliencie.


Pozwól mi wyjaśnić. Podstawowym dokumentem jest zwykły HTML, a nie .erb, dlatego nie mogę go użyć <%= csrf_meta_tags %>. Pomyślałem, że wystarczy protect_from_forgerytylko wspomnieć . Co robić? Podstawowym dokumentem musi być zwykły HTML (nie jestem tym, który wybiera).
— Paul,

3
Kiedy używasz protect_from_forgerytego, co mówisz, to „kiedy mój kod JavaScript wysyła żądania Ajax, obiecuję wysłać X-CSRF-Tokenw nagłówku symbol odpowiadający aktualnemu tokenowi CSRF”. Aby uzyskać ten token, Railsy wstrzykują go do DOM za pomocą <%= csrf_meta_token %>i pobierają zawartość metatagu za pomocą jQuery za każdym razem, gdy wysyła żądania Ajax (domyślny sterownik UJS Rails 3 robi to za Ciebie). Jeśli nie używasz ERB, nie ma sposobu, aby pobrać aktualny token z Railsów na stronę i / lub JavaScript - i dlatego nie możesz używać protect_from_forgeryw ten sposób.
— Michelle Tilley,

Dziękuję za wyjaśnienie. To, co myślałem, że w klasycznej aplikacji po stronie serwera strona klienta otrzymuje za csrf_meta_tagskażdym razem, gdy serwer generuje odpowiedź, i za każdym razem te tagi różnią się od poprzednich. Zatem te tagi są unikalne dla każdego żądania. Pytanie brzmi: w jaki sposób aplikacja otrzymuje te tagi dla żądania AJAX (bez kąta)? Użyłem protect_from_forgery z żądaniami jQuery POST, nigdy nie zawracałem sobie głowy uzyskaniem tego tokena CSRF i zadziałało. W jaki sposób?
— Paul,

1
Sterownik Rails UJS wykorzystuje, jQuery.ajaxPrefilterjak pokazano tutaj: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/ ... Możesz przejrzeć ten plik i zobaczyć wszystkie obręcze, przez które przechodzą Railsy, ​​aby działał prawie bez konieczności martw się o to.
— Michelle Tilley

@BrandonTilley nie byłoby sensu robią to tylko puti postzamiast na common? Z przewodnika po zabezpieczeniach szyn :The solution to this is including a security token in non-GET requests
— christianvuerings

29

Angular_rails_csrf gem automatycznie dodaje wsparcie dla wzoru opisanego w odpowiedzi HungYuHei jest do wszystkich kontrolerów:

# Gemfile
gem 'angular_rails_csrf'

masz pomysł, jak skonfigurować kontroler aplikacji i inne ustawienia związane z csrf / fałszerstwem, aby poprawnie używać angular_rails_csrf?
— Ben Wheeler

W chwili pisania tego komentarza angular_rails_csrfgem nie działa z Railsami 5. Jednak skonfigurowanie nagłówków żądań Angular z wartością z metatagu CSRF działa!
— bideowego

Pojawiła się nowa wersja gem, która obsługuje Rails 5.
— jsanders

4

Odpowiedź, która łączy wszystkie poprzednie odpowiedzi i opiera się na używaniu Deviseklejnotu uwierzytelniania.

Przede wszystkim dodaj klejnot:

gem 'angular_rails_csrf'

Następnie dodaj rescue_fromblok do 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

Na koniec dodaj moduł przechwytujący do aplikacji kątowej.

# 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
Dlaczego wstrzykujesz $injectorzamiast tylko bezpośrednio $http?
— whitehat101

To działa, ale myślę, że dodałem tylko sprawdzenie, czy żądanie zostało już powtórzone. Po powtórzeniu nie wysyłamy ponownie, ponieważ będzie się zapętlać na zawsze.
— duleorlovic

1

Widziałem inne odpowiedzi i pomyślałem, że są świetne i dobrze przemyślane. Moja aplikacja rails działała jednak z czymś, co uważałem za prostsze rozwiązanie, więc pomyślałem, że się nim podzielę. Moja aplikacja rails zawiera to domyślne ustawienie,

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

Przeczytałem komentarze i wydawało mi się, że właśnie tego chcę użyć kątowego i uniknąć błędu csrf. Zmieniłem to na to,

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

A teraz działa! Nie widzę powodu, dla którego to nie powinno działać, ale chciałbym usłyszeć spostrzeżenia z innych plakatów.


6
spowoduje to problemy, jeśli próbujesz użyć sesji railsowych, ponieważ zostanie ustawiony na zero, jeśli nie przejdzie testu fałszerstwa, co zawsze będzie, ponieważ nie wysyłasz tokena csrf po stronie klienta.
— hajpoj

Ale jeśli nie używasz sesji Railsowych, wszystko jest w porządku; Dziękuję Ci! Starałem się znaleźć najczystsze rozwiązanie tego problemu.
— Morgan

1

W mojej aplikacji wykorzystałem treść odpowiedzi HungYuHei. Zauważyłem, że miałem do czynienia z kilkoma dodatkowymi problemami, jednak niektóre z powodu używania Devise do uwierzytelniania, a niektóre z powodu domyślnego ustawienia mojej aplikacji:

protect_from_forgery with: :exception

Zwracam uwagę na powiązane pytanie dotyczące przepełnienia stosu i odpowiedzi , a następnie napisałem znacznie bardziej szczegółowy wpis na blogu, który podsumowuje różne rozważania. Części tego rozwiązania, które są tutaj istotne, to w kontrolerze aplikacji:

  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

Znalazłem bardzo szybki sposób na to. Wszystko, co musiałem zrobić, to:

za. Moim zdaniem inicjalizuję $scopezmienną, która zawiera token, powiedzmy przed formularzem, a jeszcze lepiej przy inicjalizacji kontrolera:

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

b. W moim kontrolerze AngularJS przed zapisaniem nowego wpisu dodaję token do skrótu:

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

Nic więcej nie trzeba robić.


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

Działa po stronie angularjs!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.