Rails CSRF Protection + Angular.js: protect_from_forgery mi fa disconnettere da POST


129

Se l' protect_from_forgeryopzione è menzionata in application_controller, allora posso accedere ed eseguire qualsiasi richiesta GET, ma alla prima richiesta POST Rails resetta la sessione, il che mi disconnette.

Ho protect_from_forgerydisattivato temporaneamente l' opzione, ma vorrei usarla con Angular.js. C'è un modo per farlo?


Vedere se questo aiuta qualsiasi, la sua impostazione su HTTP headers stackoverflow.com/questions/14183025/...
Mark Rajcok

Risposte:


276

Penso che leggere il valore CSRF da DOM non sia una buona soluzione, è solo una soluzione alternativa.

Ecco un modulo del sito web ufficiale angularJS http://docs.angularjs.org/api/ng.$http :

Dal momento che solo JavaScript che viene eseguito sul tuo dominio potrebbe leggere il cookie, il tuo server può essere certo che l'XHR provenga da JavaScript in esecuzione sul tuo dominio.

Per trarne vantaggio (protezione CSRF), il tuo server deve impostare un token in un cookie di sessione leggibile JavaScript chiamato XSRF-TOKEN alla prima richiesta GET HTTP. Nelle successive richieste non GET il server può verificare che il cookie corrisponda all'intestazione HTTP X-XSRF-TOKEN

Ecco la mia soluzione basata su queste istruzioni:

Innanzitutto, imposta il 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

Quindi, dovremmo verificare il token su ogni richiesta non GET.
Poiché Rails ha già creato un metodo simile, possiamo semplicemente ignorarlo per aggiungere la nostra logica:

# 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
Mi piace questa tecnica, in quanto non è necessario modificare alcun codice sul lato client.
Michelle Tilley,

11
In che modo questa soluzione preserva l'utilità della protezione CSRF? Impostando il cookie, il browser dell'utente contrassegnato invierà quel cookie su tutte le richieste successive, comprese le richieste tra siti. Potrei creare un sito di terze parti dannoso che invia una richiesta malevola e il browser dell'utente invierebbe "XSRF-TOKEN" al server. Sembra che questa soluzione equivale a disattivare del tutto la protezione CSRF.
Steven,

9
Dai documenti angolari: "Poiché solo JavaScript che viene eseguito sul tuo dominio è in grado di leggere il cookie, il tuo server può essere certo che l'XHR provenga da JavaScript in esecuzione sul tuo dominio". @StevenXu - In che modo il sito di terze parti legge i cookie?
Jimmy Baker,

8
@JimmyBaker: sì, hai ragione. Ho esaminato la documentazione. L'approccio è concettualmente valido. Ho confuso l'impostazione del cookie con la convalida, non rendendomi conto che Angular il framework stava impostando un'intestazione personalizzata in base al valore del cookie!
Steven,

5
form_authenticity_token genera nuovi valori su ogni chiamata in Rails 4.2, quindi questo non sembra funzionare più.
Dave,

78

Se stai usando la protezione CSRF predefinita di Rails ( <%= csrf_meta_tags %>), puoi configurare il tuo modulo angolare in questo modo:

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

Oppure, se non stai usando CoffeeScript (che cosa !?):

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

Se preferisci, puoi inviare l'intestazione solo su richieste non GET con qualcosa di simile al seguente:

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
]

Inoltre, assicurati di controllare la risposta di HungYuHei , che copre tutte le basi sul server anziché sul client.


Lasciatemi spiegare. Il documento di base è un semplice HTML, non .erb quindi non posso usarlo <%= csrf_meta_tags %>. Ho pensato che ci dovrebbe essere abbastanza per menzionare protect_from_forgerysolo. Cosa fare? Il documento di base deve essere un semplice HTML (non sono io quello che sceglie).
Paul,

3
Quando usi protect_from_forgeryquello che stai dicendo è "quando il mio codice JavaScript invia richieste Ajax, prometto di inviare un X-CSRF-Tokennell'intestazione che corrisponde al token CSRF corrente". Per ottenere questo token, Rails lo inietta nel DOM <%= csrf_meta_token %>e ottiene il contenuto del metatag con jQuery ogni volta che effettua richieste Ajax (il driver UJS predefinito di Rails 3 lo fa per te). Se non stai utilizzando ERB, non c'è modo di ottenere il token corrente da Rails nella pagina e / o JavaScript - e quindi non puoi usarlo protect_from_forgeryin questo modo.
Michelle Tilley,

Grazie per la spiegazione. Cosa ho pensato che in un'applicazione classica lato server il lato client riceve csrf_meta_tagsogni volta che il server genera una risposta e ogni volta che questi tag sono diversi da quelli precedenti. Quindi, questi tag sono unici per ogni richiesta. La domanda è: come l'applicazione riceve questi tag per una richiesta AJAX (senza angolare)? Ho usato protect_from_forgery con richieste POST jQuery, non mi sono mai preoccupato di ottenere questo token CSRF e ha funzionato. Come?
Paul,

1
Il driver UJS di Rails utilizza jQuery.ajaxPrefiltercome mostrato qui: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/… Puoi sfogliare questo file e vedere tutti i cerchi che salta Rails per farlo funzionare praticamente senza farlo preoccuparsene.
Michelle Tilley,

@BrandonTilley non avrebbe senso farlo solo su pute postanziché su common? Dalla guida alla sicurezza delle rotaie :The solution to this is including a security token in non-GET requests
christianvuerings,

29

La gemma angular_rails_csrf aggiunge automaticamente il supporto per lo schema descritto nella risposta di HungYuHei a tutti i tuoi controller:

# Gemfile
gem 'angular_rails_csrf'

qualche idea su come configurare il controller dell'applicazione e altre impostazioni relative a csrf / forgery per utilizzare correttamente angular_rails_csrf?
Ben Wheeler,

Al momento di questo commento la angular_rails_csrfgemma non funziona con Rails 5. Tuttavia, la configurazione delle intestazioni delle richieste angolari con il valore dal metatag CSRF funziona!
bideowego,

C'è una nuova versione della gemma, che supporta Rails 5.
jsanders il

4

La risposta che unisce tutte le risposte precedenti e fa affidamento sul fatto che stai utilizzando la Devisegemma di autenticazione.

Prima di tutto, aggiungi la gemma:

gem 'angular_rails_csrf'

Quindi, aggiungi il rescue_fromblocco in 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

E infine, aggiungi il modulo intercettore alla tua app angolare.

# 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
Perché stai iniettando $injectorinvece di iniettare direttamente $http?
whitehat101,

Funziona, ma penso solo che ho aggiunto è controllare se la richiesta è già stata ripetuta. Quando è stato ripetuto, non inviamo di nuovo poiché continuerà a scorrere per sempre.
duleorlovic,

1

Ho visto le altre risposte e ho pensato che fossero grandiose e ben ponderate. Ho fatto funzionare la mia app Rails con quella che pensavo fosse una soluzione più semplice, quindi ho pensato di condividere. La mia app rails è arrivata con questa impostazione predefinita,

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

Ho letto i commenti e mi è sembrato quello che voglio usare angolare ed evitare l'errore CSRF. L'ho cambiato in questo,

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

E ora funziona! Non vedo alcun motivo per cui questo non dovrebbe funzionare, ma mi piacerebbe sentire alcune intuizioni da altri poster.


6
ciò causerà problemi se si sta tentando di utilizzare le "sessioni" delle rotaie poiché verrà impostato su zero se non supera il test di falsificazione, che sarebbe sempre, poiché non si invia il token csrf dal lato client.
hajpoj,

Ma se non stai usando le sessioni di Rails va tutto bene; grazie! Ho avuto difficoltà a trovare la soluzione più pulita a questo.
Morgan,

1

Ho usato il contenuto della risposta di HungYuHei nella mia applicazione. Ho scoperto che avevo a che fare con alcuni problemi aggiuntivi, alcuni a causa del mio uso di Devise per l'autenticazione e alcuni a causa dell'impostazione predefinita che ho ottenuto con la mia applicazione:

protect_from_forgery with: :exception

Ho notato la relativa domanda di overflow dello stack e le risposte lì e ho scritto un post sul blog molto più dettagliato che riassume le varie considerazioni. Le parti di quella soluzione che sono rilevanti qui sono, nel controller dell'applicazione:

  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

Ho trovato un trucco molto veloce per questo. Tutto quello che dovevo fare è il seguente:

un. A mio avviso, inizializzo una $scopevariabile che contiene il token, diciamo prima del modulo, o ancora meglio all'inizializzazione del controller:

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

b. Nel mio controller AngularJS, prima di salvare la mia nuova voce, aggiungo il token all'hash:

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

Non è necessario fare altro.


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

Funziona sul lato angularjs!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.