Proteção Rails CSRF + Angular.js: protect_from_forgery me faz sair do POST

129

Se a protect_from_forgeryopção for mencionada em application_controller, eu posso efetuar login e executar quaisquer solicitações GET, mas na primeira solicitação POST, o Rails redefine a sessão, o que me desconecta.

protect_from_forgeryDesativei a opção temporariamente, mas gostaria de usá-la com o Angular.js. Existe alguma maneira de fazer isso?

Paulo
fonte
Ver se isso ajuda qualquer, sua localização sobre cabeçalhos HTTP stackoverflow.com/questions/14183025/...
Mark Rajcok

Respostas:

276

Acho que ler o valor CSRF do DOM não é uma boa solução, é apenas uma solução alternativa.

Aqui está um formulário no site oficial do angularJS http://docs.angularjs.org/api/ng.$http :

Como apenas o JavaScript executado em seu domínio pode ler o cookie, seu servidor pode ter certeza de que o XHR veio do JavaScript em execução no seu domínio.

Para aproveitar isso (Proteção CSRF), seu servidor precisa definir um token em um cookie de sessão legível por JavaScript chamado XSRF-TOKEN na primeira solicitação HTTP GET. Em solicitações não-GET subsequentes, o servidor pode verificar se o cookie corresponde ao cabeçalho HTTP X-XSRF-TOKEN

Aqui está minha solução com base nessas instruções:

Primeiro, defina o 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

Em seguida, devemos verificar o token em todas as solicitações não GET.
Como o Rails já construiu com o método semelhante, podemos simplesmente substituí-lo para acrescentar nossa 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
fonte
18
Eu gosto dessa técnica, pois você não precisa modificar nenhum código do lado do cliente.
Michelle Tilley
11
Como essa solução preserva a utilidade da proteção contra CSRF? Ao definir o cookie, o navegador do usuário marcado enviará esse cookie em todas as solicitações subsequentes, incluindo solicitações entre sites. Eu poderia configurar um site malicioso de terceiros que enviasse uma solicitação maliciosa e o navegador do usuário enviaria 'XSRF-TOKEN' para o servidor. Parece que esta solução é equivalente a desativar completamente a proteção CSRF.
19413 Steven
9
Na documentação do Angular: "Como apenas o JavaScript executado em seu domínio pode ler o cookie, seu servidor pode ter certeza de que o XHR veio do JavaScript em execução no seu domínio". @StevenXu - Como o site de terceiros leria o cookie?
Jimmy Baker
8
@ JimmyBaker: sim, você está certo. Eu revi a documentação. A abordagem é conceitualmente sólida. Confundi a configuração do cookie com a validação, sem perceber que a estrutura Angular estava configurando um cabeçalho personalizado com base no valor do cookie!
Steven
5
form_authenticity_token gera novos valores em cada chamada no Rails 4.2, portanto isso parece não funcionar mais.
Dave
78

Se você estiver usando a proteção padrão do Rails CSRF ( <%= csrf_meta_tags %>), poderá configurar seu módulo Angular assim:

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

Ou, se você não estiver usando o CoffeeScript (o que !?):

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

Se preferir, você pode enviar o cabeçalho apenas em solicitações não GET com algo como o seguinte:

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
]

Além disso, verifique a resposta do HungYuHei , que cobre todas as bases do servidor e não do cliente.

Michelle Tilley
fonte
Deixe-me explicar. O documento base é um HTML simples, não .erb, portanto, não posso usar <%= csrf_meta_tags %>. Eu pensei que deveria haver o suficiente para mencionar protect_from_forgeryapenas. O que fazer? O documento base deve ser um HTML simples (aqui não sou eu quem escolhe).
Paul
3
Quando você usa protect_from_forgeryo que está dizendo é "quando meu código JavaScript faz solicitações do Ajax, prometo enviar um X-CSRF-Tokenno cabeçalho que corresponda ao token CSRF atual". Para obter esse token, o Rails o injeta no DOM <%= csrf_meta_token %>e obtém o conteúdo da metatag com o jQuery sempre que ele solicita o Ajax (o driver UJS padrão do Rails 3 faz isso por você). Se você não estiver usando o ERB, não há como inserir o token atual do Rails na página e / ou no JavaScript - e, portanto, você não pode usá protect_from_forgery-lo dessa maneira.
31513 Michelle Michelle Tilley
Obrigado pela explicação. O que eu pensava que em um aplicativo clássico do lado do servidor, o lado do cliente recebe csrf_meta_tagscada vez que o servidor gera uma resposta e cada vez que essas tags são diferentes das anteriores. Portanto, essas tags são exclusivas para cada solicitação. A questão é: como o aplicativo recebe essas tags para uma solicitação AJAX (sem angular)? Usei protect_from_forgery com solicitações jQuery POST, nunca me incomodei em obter esse token CSRF e funcionou. Quão?
Paul
1
O driver UJS do Rails usa jQuery.ajaxPrefiltercomo mostrado aqui: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/… Você pode ler este arquivo e ver todos os aros que o Rails percorre para fazê-lo funcionar praticamente sem precisar preocupado com isso.
31513 Michelle Michelle Tilley
@BrandonTilley, não faria sentido fazer isso apenas pute postnão em vez disso common? A partir do guia de segurança trilhos :The solution to this is including a security token in non-GET requests
christianvuerings
29

A gem angular_rails_csrf adiciona automaticamente suporte ao padrão descrito na resposta de HungYuHei a todos os seus controladores:

# Gemfile
gem 'angular_rails_csrf'
jsanders
fonte
alguma idéia de como você deve configurar seu controlador de aplicativo e outras configurações relacionadas ao csrf / falsificação, para usar o angular_rails_csrf corretamente?
Ben Wheeler
No momento deste comentário, o angular_rails_csrfgem não funciona com o Rails 5. No entanto, a configuração de cabeçalhos de solicitação Angular com o valor da metatag CSRF funciona!
precisa saber é
Há uma nova versão do gem, que suporta Rails 5.
jsanders
4

A resposta que mescla todas as respostas anteriores e conta que você está usando a Devisejóia de autenticação.

Primeiro de tudo, adicione a gema:

gem 'angular_rails_csrf'

Em seguida, adicione rescue_frombloco no 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, finalmente, adicione o módulo interceptador ao seu aplicativo 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
fonte
1
Por que você está injetando em $injectorvez de apenas injetando diretamente $http?
precisa saber é o seguinte
Isso funciona, mas acho que apenas adicionei é verificar se a solicitação já foi repetida. Quando foi repetido, não enviamos novamente, pois ele será repetido para sempre.
Duleorlovic 29/09/16
1

Vi as outras respostas e achei que eram ótimas e bem pensadas. Embora meu aplicativo Rails funcionasse com o que eu pensava ser uma solução mais simples, pensei em compartilhar. Meu aplicativo rails veio com esse padrão,

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

Eu li os comentários e parecia que é isso que eu quero usar angular e evitar o erro csrf. Eu mudei para isso,

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 agora funciona! Não vejo nenhuma razão para que isso não funcione, mas eu adoraria ouvir algumas dicas de outros pôsteres.

Blaine Hatab
fonte
6
isso causará problemas se você estiver tentando usar 'sessões' dos trilhos, pois será definido como nulo se falhar no teste de falsificação, o que seria sempre, pois você não está enviando o token csrf do lado do cliente.
hajpoj
Mas se você não estiver usando sessões do Rails, tudo estará bem; obrigado! Eu tenho lutado para encontrar a solução mais limpa para isso.
Morgan
1

Eu usei o conteúdo da resposta de HungYuHei no meu aplicativo. No entanto, descobri que estava lidando com alguns problemas adicionais, alguns por causa do uso do Devise para autenticação e outros por causa do padrão que obtive com meu aplicativo:

protect_from_forgery with: :exception

Observo a pergunta relacionada ao estouro de pilha e as respostas lá e escrevi uma postagem de blog muito mais detalhada que resume as várias considerações. As partes dessa solução que são relevantes aqui são, no controlador de aplicativo:

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

Eu encontrei um hack muito rápido para isso. Tudo o que eu precisava fazer era o seguinte:

uma. Na minha opinião, eu inicializo uma $scopevariável que contém o token, digamos antes do formulário, ou ainda melhor na inicialização do controlador:

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

b. No meu controlador AngularJS, antes de salvar minha nova entrada, adiciono o token ao hash:

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

Nada mais precisa ser feito.

Ruby Racer
fonte
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á trabalhando no lado do angularjs!

Evgeniy Krokhmal
fonte