Rails CSRF Protection + Angular.js: protect_from_forgery me fait me déconnecter sur POST

129

Si l' protect_from_forgeryoption est mentionnée dans application_controller, alors je peux me connecter et effectuer toutes les requêtes GET, mais à la toute première requête POST, Rails réinitialise la session, ce qui me déconnecte.

J'ai protect_from_forgerydésactivé temporairement l' option, mais j'aimerais l'utiliser avec Angular.js. Y a-t-il un moyen de faire cela?

Paul
la source
Voyez si cela aide, il s'agit de définir les en-têtes HTTP stackoverflow.com/questions/14183025/…
Mark Rajcok

Réponses:

276

Je pense que lire la valeur CSRF à partir de DOM n'est pas une bonne solution, c'est juste une solution de contournement.

Voici un document du site officiel angularJS http://docs.angularjs.org/api/ng.$http :

Étant donné que seul le JavaScript qui s'exécute sur votre domaine peut lire le cookie, votre serveur peut être assuré que le XHR provient de JavaScript s'exécutant sur votre domaine.

Pour tirer parti de cette (protection CSRF), votre serveur doit définir un jeton dans un cookie de session lisible par JavaScript appelé XSRF-TOKEN lors de la première requête HTTP GET. Lors des demandes non GET suivantes, le serveur peut vérifier que le cookie correspond à l'en-tête HTTP X-XSRF-TOKEN

Voici ma solution basée sur ces instructions:

Tout d'abord, définissez le 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

Ensuite, nous devons vérifier le jeton sur chaque requête non GET.
Puisque Rails a déjà construit avec la méthode similaire, nous pouvons simplement la remplacer pour ajouter notre logique:

# 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
la source
18
J'aime cette technique, car vous n'avez pas à modifier de code côté client.
Michelle Tilley
11
Comment cette solution préserve-t-elle l'utilité de la protection CSRF? En définissant le cookie, le navigateur de l'utilisateur marqué enverra ce cookie sur toutes les demandes ultérieures, y compris les demandes intersites. Je pourrais mettre en place un site tiers malveillant qui enverrait une demande malveillante et le navigateur de l'utilisateur enverrait «XSRF-TOKEN» au serveur. Il semble que cette solution équivaut à désactiver complètement la protection CSRF.
Steven
9
Extrait de la documentation Angular: "Étant donné que seul JavaScript qui s'exécute sur votre domaine peut lire le cookie, votre serveur peut être assuré que le XHR provient de JavaScript s'exécutant sur votre domaine." @StevenXu - Comment le site tiers lirait-il le cookie?
Jimmy Baker
8
@JimmyBaker: oui, vous avez raison. J'ai examiné la documentation. L'approche est conceptuellement valable. J'ai confondu le paramétrage du cookie avec la validation, ne réalisant pas qu'Angular le framework définissait un en-tête personnalisé en fonction de la valeur du cookie!
Steven
5
form_authenticity_token génère de nouvelles valeurs à chaque appel dans Rails 4.2, donc cela ne semble plus fonctionner.
Dave
78

Si vous utilisez la protection Rails CSRF par défaut ( <%= csrf_meta_tags %>), vous pouvez configurer votre module angulaire comme ceci:

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

Ou, si vous n'utilisez pas CoffeeScript (quoi !?):

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

Si vous préférez, vous pouvez envoyer l'en-tête uniquement sur les demandes non GET avec quelque chose comme ce qui suit:

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
]

Assurez-vous également de consulter la réponse de HungYuHei , qui couvre toutes les bases sur le serveur plutôt que sur le client.

Michelle Tilley
la source
Laisse-moi expliquer. Le document de base est un simple HTML, pas .erb donc je ne peux pas l'utiliser <%= csrf_meta_tags %>. J'ai pensé qu'il devrait y en avoir assez pour mentionner protect_from_forgeryseulement. Que faire? Le document de base doit être un simple HTML (je ne suis pas ici celui qui choisit).
Paul
3
Lorsque vous utilisez protect_from_forgeryce que vous dites, c'est "lorsque mon code JavaScript fait des requêtes Ajax, je promets d'envoyer un X-CSRF-Tokendans l'en-tête qui correspond au jeton CSRF actuel". Afin d'obtenir ce jeton, Rails l'injecte dans le DOM avec <%= csrf_meta_token %>et obtient le contenu de la balise meta avec jQuery chaque fois qu'il fait des requêtes Ajax (le pilote Rails 3 UJS par défaut le fait pour vous). Si vous n'utilisez pas ERB, il n'y a aucun moyen d'obtenir le jeton actuel de Rails dans la page et / ou le JavaScript - et vous ne pouvez donc pas l'utiliser protect_from_forgeryde cette manière.
Michelle Tilley
Merci pour votre explication. Ce que je pensais que dans une application classique côté serveur, le côté client reçoit csrf_meta_tagschaque fois que le serveur génère une réponse, et chaque fois que ces balises sont différentes des précédentes. Ainsi, ces balises sont uniques pour chaque demande. La question est: comment l'application reçoit ces balises pour une requête AJAX (sans angulaire)? J'ai utilisé protect_from_forgery avec les requêtes jQuery POST, je ne me suis jamais soucié d'obtenir ce jeton CSRF, et cela a fonctionné. Comment?
Paul
1
Le pilote Rails UJS utilise jQuery.ajaxPrefiltercomme indiqué ici: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets / ... Vous pouvez parcourir ce fichier et voir tous les cerceaux que Rails saute pour le faire fonctionner à peu près sans avoir à inquiétez-vous à ce sujet.
Michelle Tilley
@BrandonTilley n'aurait-il pas de sens de ne faire cela que sur putet postau lieu de common? Du guide de sécurité des rails :The solution to this is including a security token in non-GET requests
christianvuerings
29

La gemme angular_rails_csrf ajoute automatiquement le support du modèle décrit dans la réponse de HungYuHei à tous vos contrôleurs:

# Gemfile
gem 'angular_rails_csrf'
jsanders
la source
une idée de la façon dont vous devez configurer votre contrôleur d'application et d'autres paramètres liés à csrf / falsification, pour utiliser correctement angular_rails_csrf?
Ben Wheeler
Au moment de ce commentaire, la angular_rails_csrfgemme ne fonctionne pas avec Rails 5. Cependant, la configuration des en-têtes de requête Angular avec la valeur de la balise meta CSRF fonctionne!
bideowego
Il y a une nouvelle version du gem, qui prend en charge Rails 5.
jsanders
4

La réponse qui fusionne toutes les réponses précédentes et repose sur le fait que vous utilisez Deviseune gemme d'authentification.

Tout d'abord, ajoutez la gemme:

gem 'angular_rails_csrf'

Ensuite, ajoutez un rescue_frombloc dans 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

Et enfin, ajoutez le module intercepteur à votre application angulaire.

# 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
la source
1
Pourquoi vous injectez-vous $injectorau lieu de vous injecter directement $http?
whitehat101
Cela fonctionne, mais je pense seulement que j'ai ajouté est de vérifier si la demande est déjà répétée. Quand il a été répété, nous n'envoyons plus car il bouclera pour toujours.
duleorlovic le
1

J'ai vu les autres réponses et j'ai pensé qu'elles étaient excellentes et bien pensées. J'ai fait fonctionner mon application de rails avec ce que je pensais être une solution plus simple, alors j'ai pensé partager. Mon application rails est livrée avec ce paramètre par défaut,

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

J'ai lu les commentaires et il semblait que c'était ce que je voulais utiliser angular et éviter l'erreur csrf. Je l'ai changé en ceci,

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

Et maintenant ça marche! Je ne vois aucune raison pour laquelle cela ne devrait pas fonctionner, mais j'aimerais beaucoup entendre d'autres affiches.

Blaine Hatab
la source
6
cela posera des problèmes si vous essayez d'utiliser des «sessions» de rails car il sera mis à nil s'il échoue au test de contrefaçon, ce qui serait toujours le cas, puisque vous n'envoyez pas le jeton csrf du côté client.
hajpoj
Mais si vous n'utilisez pas de sessions Rails, tout va bien; Merci! J'ai eu du mal à trouver la solution la plus propre à cela.
Morgan
1

J'ai utilisé le contenu de la réponse de HungYuHei dans mon application. J'ai constaté que je rencontrais cependant quelques problèmes supplémentaires, certains à cause de mon utilisation de Devise pour l'authentification, et d'autres à cause du défaut que j'ai obtenu avec mon application:

protect_from_forgery with: :exception

Je note la question relative au dépassement de pile et les réponses , et j'ai écrit un article de blog beaucoup plus détaillé qui résume les différentes considérations. Les parties de cette solution qui sont pertinentes ici sont, dans le contrôleur d'application:

  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
la source
1

J'ai trouvé un hack très rapide à cela. Tout ce que j'avais à faire est le suivant:

une. À mon avis, j'initialise une $scopevariable qui contient le jeton, disons avant le formulaire, ou encore mieux à l'initialisation du contrôleur:

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

b. Dans mon contrôleur AngularJS, avant d'enregistrer ma nouvelle entrée, j'ajoute le jeton au hachage:

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

Il n'y a plus rien à faire.

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

Ça marche du côté angularjs!

Evgeniy Krokhmal
la source