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ć?

Paweł
źródło
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
HungYuHei
źródło
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.

Michelle Tilley
źródło
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'
jsanders
źródło
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')
Anton Orel
źródło
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.

Blaine Hatab
źródło
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
Paul L.
źródło
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ć.

Ruby Racer
źródło
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!

Evgeniy Krokhmal
źródło