Protección CSRF de Rails + Angular.js: protect_from_forgery me hace cerrar sesión en POST

129

Si la protect_from_forgeryopción se menciona en application_controller, puedo iniciar sesión y realizar cualquier solicitud GET, pero en la primera solicitud POST Rails restablece la sesión, lo que me desconecta.

protect_from_forgeryDesactivé la opción temporalmente, pero me gustaría usarla con Angular.js. ¿Hay alguna forma de hacer eso?

Pablo
fuente
Vea si esto ayuda, se trata de configurar encabezados HTTP stackoverflow.com/questions/14183025/…
Mark Rajcok

Respuestas:

276

Creo que leer el valor CSRF de DOM no es una buena solución, es solo una solución.

Aquí hay un documento del sitio web oficial angularJS http://docs.angularjs.org/api/ng.$http :

Dado que solo JavaScript que se ejecuta en su dominio podría leer la cookie, su servidor puede estar seguro de que el XHR provino de JavaScript que se ejecuta en su dominio.

Para aprovechar esto (Protección CSRF), su servidor necesita establecer un token en una cookie de sesión legible de JavaScript llamada XSRF-TOKEN en la primera solicitud HTTP GET. En solicitudes posteriores que no sean GET, el servidor puede verificar que la cookie coincida con el encabezado HTTP X-XSRF-TOKEN

Aquí está mi solución basada en esas instrucciones:

Primero, configure la 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

Luego, debemos verificar el token en cada solicitud que no sea GET.
Como Rails ya ha construido con un método similar, simplemente podemos anularlo para agregar nuestra lógica:

# 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
fuente
18
Me gusta esta técnica, ya que no tiene que modificar ningún código del lado del cliente.
Michelle Tilley
11
¿Cómo preserva esta solución la utilidad de la protección CSRF? Al configurar la cookie, el navegador del usuario marcado enviará esa cookie en todas las solicitudes posteriores, incluidas las solicitudes entre sitios. Podría configurar un sitio de terceros malicioso que envíe una solicitud maliciosa y el navegador del usuario enviaría 'XSRF-TOKEN' al servidor. Parece que esta solución equivale a desactivar la protección CSRF por completo.
Steven
9
De los documentos de Angular: "Dado que solo JavaScript que se ejecuta en su dominio puede leer la cookie, su servidor puede estar seguro de que el XHR proviene de JavaScript que se ejecuta en su dominio". @StevenXu: ¿cómo leería la cookie el sitio de terceros?
Jimmy Baker
8
@ JimmyBaker: sí, tienes razón. He revisado la documentación. El enfoque es conceptualmente sólido. ¡Confundí la configuración de la cookie con la validación, sin darme cuenta de que Angular el marco estaba configurando un encabezado personalizado basado en el valor de la cookie!
Steven
55
form_authenticity_token genera nuevos valores en cada llamada en Rails 4.2, por lo que parece que ya no funciona.
Dave
78

Si está utilizando la protección CSRF Rails predeterminada ( <%= csrf_meta_tags %>), puede configurar su módulo angular de esta manera:

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

O, si no estás usando CoffeeScript (¿qué?):

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

Si lo prefiere, puede enviar el encabezado solo en solicitudes no GET con algo como lo siguiente:

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
]

Además, asegúrese de consultar la respuesta de HungYuHei , que cubre todas las bases del servidor en lugar del cliente.

Michelle Tilley
fuente
Dejame explicar. El documento base es un HTML simple, no .erb, por lo tanto, no puedo usarlo <%= csrf_meta_tags %>. Pensé que debería haber suficiente para mencionar protect_from_forgerysolo. ¿Qué hacer? El documento base debe ser un HTML simple (aquí no soy el que elige).
Paul
3
Cuando usa protect_from_forgerylo que dice es "cuando mi código JavaScript hace solicitudes Ajax, prometo enviar un X-CSRF-Tokenencabezado que corresponda al token CSRF actual". Para obtener este token, Rails lo inyecta en el DOM <%= csrf_meta_token %>y obtiene el contenido de la metaetiqueta con jQuery cada vez que realiza solicitudes Ajax (el controlador UJS predeterminado de Rails 3 lo hace por usted). Si no está utilizando ERB, no hay forma de obtener el token actual de Rails en la página y / o el JavaScript, por lo que no puede usarlo protect_from_forgeryde esta manera.
Michelle Tilley
Gracias por la explicación. Lo que pensé que en una aplicación clásica del lado del servidor, el lado del cliente recibe csrf_meta_tagscada vez que el servidor genera una respuesta, y cada vez que estas etiquetas son diferentes de las anteriores. Por lo tanto, estas etiquetas son únicas para cada solicitud. La pregunta es: ¿cómo recibe la aplicación estas etiquetas para una solicitud AJAX (sin angular)? Utilicé protect_from_forgery con solicitudes jQuery POST, nunca me molesté en obtener este token CSRF, y funcionó. ¿Cómo?
Paul
1
El controlador Rails UJS utiliza jQuery.ajaxPrefiltercomo se muestra aquí: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets /... Puede leer detenidamente este archivo y ver todos los aros que Rails salta para que funcione bastante sin tener que Preocúpate por eso.
Michelle Tilley
@BrandonTilley ¿No tendría sentido no solamente hacer esto por puty posten lugar de sobre common? De la guía de seguridad de rieles :The solution to this is including a security token in non-GET requests
christianvuerings
29

La gema angular_rails_csrf agrega automáticamente soporte para el patrón descrito en la respuesta de HungYuHei a todos sus controladores:

# Gemfile
gem 'angular_rails_csrf'
jsanders
fuente
¿Alguna idea de cómo debe configurar el controlador de su aplicación y otras configuraciones relacionadas con csrf / forgery para utilizar angular_rails_csrf correctamente?
Ben Wheeler
En el momento de este comentario, la angular_rails_csrfgema no funciona con Rails 5. Sin embargo, ¡la configuración de encabezados de solicitud angular con el valor de la metaetiqueta CSRF funciona!
bideowego
Hay una nueva versión de la gema, que es compatible con Rails 5.
jsanders
4

La respuesta que combina todas las respuestas anteriores y confía en que está utilizando la Devisegema de autenticación.

En primer lugar, agrega la gema:

gem 'angular_rails_csrf'

Luego, agregue el rescue_frombloque en 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

Y finalmente, agregue el módulo interceptor a su aplicación angular.

# 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
fuente
1
¿Por qué estás inyectando en $injectorlugar de inyectar directamente $http?
whitehat101
Esto funciona, pero solo creo que agregué es verificar si la solicitud ya se repite. Cuando se repitió, no enviamos nuevamente ya que se repetirá para siempre.
duleorlovic
1

Vi las otras respuestas y pensé que eran geniales y bien pensadas. Sin embargo, conseguí que mi aplicación de rieles funcionara con lo que pensé que era una solución más simple, así que pensé en compartir. Mi aplicación de rieles vino con esto predeterminado,

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

Leí los comentarios y parecía que eso es lo que quiero usar angular y evitar el error csrf. Lo cambié a esto,

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

¡Y ahora funciona! No veo ninguna razón por la que esto no debería funcionar, pero me encantaría saber algo de otros carteles.

Blaine Hatab
fuente
66
esto causará problemas si está tratando de usar 'sesiones' de rails, ya que se establecerá en nulo si falla la prueba de falsificación, lo cual sería siempre, ya que no está enviando el token csrf desde el lado del cliente.
hajpoj
Pero si no estás usando sesiones de Rails, todo está bien; ¡gracias! He estado luchando por encontrar la solución más limpia para esto.
Morgan
1

He usado el contenido de la respuesta de HungYuHei en mi aplicación. Sin embargo, descubrí que estaba lidiando con algunos problemas adicionales, algunos debido a mi uso de Devise para la autenticación, y algunos debido al valor predeterminado que obtuve con mi aplicación:

protect_from_forgery with: :exception

Tomé nota de la pregunta relacionada con el desbordamiento de la pila y las respuestas allí , y escribí una publicación de blog mucho más detallada que resume las diversas consideraciones. Las partes de esa solución que son relevantes aquí son, en el controlador de la aplicación:

  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
fuente
1

Encontré un truco muy rápido para esto. Todo lo que tuve que hacer es lo siguiente:

a. En mi opinión, inicializo una $scopevariable que contiene el token, digamos antes del formulario, o incluso mejor en la inicialización del controlador:

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

si. En mi controlador AngularJS, antes de guardar mi nueva entrada, agrego el token al hash:

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

No se necesita hacer nada más.

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

¡Está trabajando en el lado angularjs!

Evgeniy Krokhmal
fuente