Rails CSRF Protection + Angular.js: Mit protected_from_forgery kann ich mich bei POST abmelden

129

Wenn die protect_from_forgeryOption in application_controller erwähnt wird, kann ich mich anmelden und alle GET-Anforderungen ausführen. Bei der ersten POST-Anforderung setzt Rails jedoch die Sitzung zurück, wodurch ich abgemeldet werde.

Ich habe die protect_from_forgeryOption vorübergehend deaktiviert, möchte sie aber mit Angular.js verwenden. Gibt es eine Möglichkeit, das zu tun?

Paul
quelle
Sehen Sie, ob dies jemandem
Mark Rajcok

Antworten:

276

Ich denke, das Lesen des CSRF-Werts aus DOM ist keine gute Lösung, sondern nur eine Problemumgehung.

Hier ist ein Dokument auf der offiziellen Website von angleJS http://docs.angularjs.org/api/ng.$http :

Da nur JavaScript, das auf Ihrer Domain ausgeführt wird, das Cookie lesen kann, kann Ihr Server sicher sein, dass das XHR von JavaScript stammt, das auf Ihrer Domain ausgeführt wird.

Um dies zu nutzen (CSRF-Schutz), muss Ihr Server bei der ersten HTTP-GET-Anforderung ein Token in einem JavaScript-lesbaren Sitzungscookie namens XSRF-TOKEN setzen. Bei nachfolgenden Nicht-GET-Anforderungen kann der Server überprüfen, ob das Cookie mit dem X-XSRF-TOKEN-HTTP-Header übereinstimmt

Hier ist meine Lösung basierend auf diesen Anweisungen:

Setzen Sie zuerst den 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

Dann sollten wir das Token bei jeder Nicht-GET-Anforderung überprüfen.
Da Rails bereits mit der ähnlichen Methode erstellt hat, können wir sie einfach überschreiben, um unsere Logik anzuhängen:

# 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
quelle
18
Ich mag diese Technik, da Sie keinen clientseitigen Code ändern müssen.
Michelle Tilley
11
Wie bewahrt diese Lösung die Nützlichkeit des CSRF-Schutzes? Durch Setzen des Cookies sendet der Browser des markierten Benutzers dieses Cookie bei allen nachfolgenden Anforderungen, einschließlich standortübergreifender Anforderungen. Ich könnte eine böswillige Website eines Drittanbieters einrichten, die eine böswillige Anfrage sendet, und der Browser des Benutzers würde 'XSRF-TOKEN' an den Server senden. Diese Lösung scheint gleichbedeutend damit zu sein, den CSRF-Schutz insgesamt auszuschalten.
Steven
9
Aus den Angular-Dokumenten: "Da nur JavaScript, das auf Ihrer Domain ausgeführt wird, das Cookie lesen kann, kann Ihr Server sicher sein, dass das XHR von JavaScript stammt, das auf Ihrer Domain ausgeführt wird." @StevenXu - Wie würde die Website eines Drittanbieters den Cookie lesen?
Jimmy Baker
8
@ JimmyBaker: Ja, du hast recht. Ich habe die Dokumentation überprüft. Der Ansatz ist konzeptionell fundiert. Ich habe die Einstellung des Cookies mit der Validierung verwechselt und nicht bemerkt, dass Angular, das Framework, einen benutzerdefinierten Header basierend auf dem Wert des Cookies festlegt!
Steven
5
form_authenticity_token generiert bei jedem Aufruf in Rails 4.2 neue Werte, sodass dies anscheinend nicht mehr funktioniert.
Dave
78

Wenn Sie den Standard-Rails-CSRF-Schutz ( <%= csrf_meta_tags %>) verwenden, können Sie Ihr Angular-Modul folgendermaßen konfigurieren:

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

Oder wenn Sie kein CoffeeScript verwenden (was!?):

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

Wenn Sie möchten, können Sie den Header nur bei Nicht-GET-Anforderungen mit den folgenden Angaben senden:

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
]

Lesen Sie auch unbedingt die Antwort von HungYuHei , die alle Grundlagen auf dem Server und nicht auf dem Client abdeckt.

Michelle Tilley
quelle
Lassen Sie mich erklären. Das Basisdokument ist ein einfaches HTML, nicht .erb, daher kann ich es nicht verwenden <%= csrf_meta_tags %>. Ich dachte, dass es genug geben sollte, um nur zu erwähnen protect_from_forgery. Was ist zu tun? Das Basisdokument muss ein einfaches HTML sein (ich bin hier nicht derjenige, der wählt).
Paul
3
Wenn Sie das verwenden, protect_from_forgerywas Sie sagen, ist "Wenn mein JavaScript-Code Ajax-Anfragen stellt, verspreche ich, eine X-CSRF-Tokenim Header zu senden , die dem aktuellen CSRF-Token entspricht." Um dieses Token zu erhalten, fügt Rails es <%= csrf_meta_token %>mit jQuery in das DOM ein und ruft den Inhalt des Meta-Tags ab, wenn Ajax-Anforderungen gestellt werden (der Standard-Rails 3-UJS-Treiber erledigt dies für Sie). Wenn Sie ERB nicht verwenden, gibt es keine Möglichkeit, das aktuelle Token von Rails in die Seite und / oder das JavaScript zu übertragen. Daher können Sie es nicht protect_from_forgeryauf diese Weise verwenden.
Michelle Tilley
Danke für die Erklärung. Was ich dachte, dass in einer klassischen serverseitigen Anwendung die Clientseite csrf_meta_tagsjedes Mal empfängt, wenn der Server eine Antwort generiert, und jedes Mal, wenn sich diese Tags von den vorherigen unterscheiden. Diese Tags sind also für jede Anforderung eindeutig. Die Frage ist: Wie empfängt die Anwendung diese Tags für eine AJAX-Anfrage (ohne Winkel)? Ich habe protected_from_forgery mit jQuery-POST-Anforderungen verwendet, mich nie darum gekümmert, dieses CSRF-Token zu erhalten, und es hat funktioniert. Wie?
Paul
1
Der UJS-Treiber von Rails wird jQuery.ajaxPrefilterwie hier gezeigt verwendet: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/… Sie können diese Datei lesen und alle Rahmen anzeigen, durch die Rails springt, damit sie praktisch funktioniert, ohne dass dies erforderlich ist Sorgen Sie sich darum.
Michelle Tilley
@BrandonTilley Wäre es nicht sinnvoll, dies nur an putund poststatt an zu tun common? Aus dem Rails Security Guide :The solution to this is including a security token in non-GET requests
Christianvuerings
29

Das Juwel angular_rails_csrf fügt allen Ihren Controllern automatisch Unterstützung für das in der Antwort von HungYuHei beschriebene Muster hinzu :

# Gemfile
gem 'angular_rails_csrf'
jsanders
quelle
Haben Sie eine Idee, wie Sie Ihren Anwendungscontroller und andere Einstellungen im Zusammenhang mit CSRF / Fälschung konfigurieren sollten, um angle_rails_csrf korrekt zu verwenden?
Ben Wheeler
Zum Zeitpunkt dieses Kommentars angular_rails_csrffunktioniert das Juwel nicht mit Rails 5. Das Konfigurieren von Angular-Anforderungsheadern mit dem Wert aus dem CSRF-Meta-Tag funktioniert jedoch!
Bideowego
Es gibt eine neue Version des Edelsteins, die Rails 5 unterstützt.
jsanders
4

Die Antwort, die alle vorherigen Antworten zusammenführt und davon abhängt, dass Sie das DeviseAuthentifizierungsjuwel verwenden.

Fügen Sie zunächst den Edelstein hinzu:

gem 'angular_rails_csrf'

Fügen Sie als Nächstes einen rescue_fromBlock in application_controller.rb hinzu:

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

Und zum Schluss fügen Sie das Interceptor-Modul zu Ihrer Winkel-App hinzu.

# 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
quelle
1
Warum injizieren Sie $injectoranstatt nur direkt zu injizieren $http?
Whitehat101
Dies funktioniert, aber ich denke nur, dass ich hinzugefügt habe, um zu überprüfen, ob die Anfrage bereits wiederholt wurde. Wenn es wiederholt wurde, senden wir nicht erneut, da es für immer wiederholt wird.
Duleorlovic
1

Ich sah die anderen Antworten und fand sie großartig und gut durchdacht. Ich habe meine Rails-App mit einer meiner Meinung nach einfacheren Lösung zum Laufen gebracht, also dachte ich, ich würde sie teilen. Meine Rails-App wurde mit dieser Standardeinstellung geliefert.

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

Ich habe die Kommentare gelesen und es schien so, als ob ich Angular verwenden und den CSRF-Fehler vermeiden möchte. Ich habe es geändert,

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

Und jetzt funktioniert es! Ich sehe keinen Grund, warum dies nicht funktionieren sollte, aber ich würde gerne Einblicke von anderen Postern erhalten.

Blaine Hatab
quelle
6
Dies führt zu Problemen, wenn Sie versuchen, Rails-Sitzungen zu verwenden, da diese auf Null gesetzt werden, wenn der Fälschungstest nicht bestanden wird. Dies ist immer der Fall, da Sie das csrf-Token nicht vom Client senden.
Hajpoj
Wenn Sie jedoch keine Rails-Sitzungen verwenden, ist alles in Ordnung. Danke! Ich habe mich bemüht, die sauberste Lösung dafür zu finden.
Morgan
1

Ich habe den Inhalt der Antwort von HungYuHei in meiner Bewerbung verwendet. Ich stellte jedoch fest, dass ich einige zusätzliche Probleme hatte, einige aufgrund der Verwendung von Devise zur Authentifizierung und einige aufgrund der Standardeinstellungen, die ich mit meiner Anwendung erhalten habe:

protect_from_forgery with: :exception

Ich notiere die zugehörige Frage zum Stapelüberlauf und die Antworten dort und habe einen viel ausführlicheren Blog-Beitrag geschrieben , in dem die verschiedenen Überlegungen zusammengefasst sind. Die Teile dieser Lösung, die hier relevant sind, sind im Anwendungscontroller:

  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
PaulL
quelle
1

Ich habe einen sehr schnellen Hack dazu gefunden. Ich musste nur Folgendes tun:

ein. Meiner Ansicht nach initialisiere ich eine $scopeVariable, die das Token enthält, beispielsweise vor dem Formular, oder noch besser bei der Controller-Initialisierung:

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

b. In meinem AngularJS-Controller füge ich vor dem Speichern meines neuen Eintrags das Token dem Hash hinzu:

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

Es muss nichts mehr getan werden.

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

Es funktioniert auf eckiger Seite!

Evgeniy Krokhmal
quelle