Rails Perlindungan CSRF + Angular.js: protect_from_forgery membuat saya logout di POST

129

Jika protect_from_forgeryopsi tersebut disebutkan dalam application_controller, maka saya bisa masuk dan melakukan permintaan GET, tetapi pada permintaan POST pertama Rails me-reset sesi, yang mengeluarkan saya.

Saya protect_from_forgerymematikan opsi untuk sementara, tetapi ingin menggunakannya dengan Angular.js. Apakah ada cara untuk melakukan itu?

Paul
sumber
Lihat apakah ini membantu, ini tentang pengaturan header HTTP stackoverflow.com/questions/14183025/...
Mark Rajcok

Jawaban:

276

Saya pikir membaca nilai CSRF dari DOM bukanlah solusi yang baik, itu hanya solusi.

Berikut ini adalah dokumen bentuk situs resmi angularJS http://docs.angularjs.org/api/ng.$http :

Karena hanya JavaScript yang berjalan di domain Anda yang dapat membaca cookie, server Anda dapat yakin bahwa XHR berasal dari JavaScript yang berjalan di domain Anda.

Untuk memanfaatkan ini (Perlindungan CSRF), server Anda perlu menetapkan token dalam cookie sesi yang dapat dibaca JavaScript yang disebut XSRF-TOKEN pada permintaan GET HTTP pertama. Pada permintaan non-GET selanjutnya server dapat memverifikasi bahwa cookie cocok dengan header HTTP X-XSRF-TOKEN

Ini solusi saya berdasarkan instruksi itu:

Pertama, atur 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

Kemudian, kita harus memverifikasi token pada setiap permintaan non-GET.
Karena Rails telah dibangun dengan metode yang serupa, kita bisa menimpanya untuk menambahkan logika kita:

# 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
sumber
18
Saya suka teknik ini, karena Anda tidak perlu mengubah kode sisi klien.
Michelle Tilley
11
Bagaimana solusi ini menjaga kegunaan perlindungan CSRF? Dengan mengatur cookie, browser pengguna yang ditandai akan mengirimkan cookie itu pada semua permintaan berikutnya termasuk permintaan lintas situs. Saya dapat mengatur situs pihak ketiga jahat yang mengirim permintaan jahat dan browser pengguna akan mengirim 'XSRF-TOKEN' ke server. Sepertinya solusi ini sama saja dengan mematikan perlindungan CSRF sama sekali.
Steven
9
Dari dokumen Angular: "Karena hanya JavaScript yang berjalan di domain Anda yang dapat membaca cookie, server Anda dapat yakin bahwa XHR berasal dari JavaScript yang berjalan di domain Anda." @StevenXu - Bagaimana situs pihak ketiga membaca cookie?
Jimmy Baker
8
@ JimmyBaker: ya, Anda benar. Saya sudah meninjau dokumentasinya. Pendekatannya secara konseptual sehat. Saya bingung pengaturan cookie dengan validasi, tidak menyadari bahwa Angular framework sedang mengatur header kustom berdasarkan nilai cookie!
Steven
5
form_authenticity_token menghasilkan nilai baru pada setiap panggilan di Rails 4.2, jadi ini sepertinya tidak berfungsi lagi.
Dave
78

Jika Anda menggunakan perlindungan CSRF Rails default ( <%= csrf_meta_tags %>), Anda dapat mengonfigurasi modul Angular Anda seperti ini:

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

Atau, jika Anda tidak menggunakan CoffeeScript (apa !?):

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

Jika suka, Anda dapat mengirim tajuk hanya pada permintaan non-GET dengan sesuatu seperti berikut:

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
]

Juga, pastikan untuk memeriksa jawaban HungYuHei , yang mencakup semua pangkalan di server daripada klien.

Michelle Tilley
sumber
Biarkan saya jelaskan. Dokumen dasar adalah HTML biasa, bukan .erb oleh karena itu saya tidak dapat menggunakan <%= csrf_meta_tags %>. Saya pikir seharusnya ada cukup untuk menyebutkan protect_from_forgerysaja. Apa yang harus dilakukan? Dokumen dasar harus berupa HTML biasa (saya di sini bukan orang yang memilih).
Paul
3
Ketika Anda menggunakan protect_from_forgeryapa yang Anda katakan adalah "ketika kode JavaScript saya membuat permintaan Ajax, saya berjanji untuk mengirim X-CSRF-Tokenheader di yang sesuai dengan token CSRF saat ini." Untuk mendapatkan token ini, Rails menyuntikkannya ke DOM dengan <%= csrf_meta_token %>dan mendapatkan isi tag meta dengan jQuery setiap kali ia membuat permintaan Ajax (driver Rails 3 UJS default melakukan ini untuk Anda). Jika Anda tidak menggunakan ERB, tidak ada cara untuk mendapatkan token saat ini dari Rails ke halaman dan / atau JavaScript - dan dengan demikian Anda tidak dapat menggunakan protect_from_forgerycara ini.
Michelle Tilley
Terima kasih atas penjelasannya. Apa yang saya pikirkan bahwa dalam aplikasi sisi server klasik, sisi klien menerima csrf_meta_tagssetiap kali server menghasilkan respons, dan setiap kali tag ini berbeda dari yang sebelumnya. Jadi, tag ini unik untuk setiap permintaan. Pertanyaannya adalah: bagaimana aplikasi menerima tag ini untuk permintaan AJAX (tanpa sudut)? Saya menggunakan protect_from_forgery dengan permintaan POST jQuery, tidak pernah repot-repot mendapatkan token CSRF ini, dan itu berhasil. Bagaimana?
Paul
1
Pengemudi Rails UJS menggunakan jQuery.ajaxPrefilterseperti yang ditunjukkan di sini: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/… Anda dapat membaca dengan teliti file ini dan melihat semua simpanan Rails melompat untuk membuatnya berfungsi cukup banyak tanpa harus khawatirkan itu.
Michelle Tilley
@BrandonTilley bukankah masuk akal hanya melakukan ini putdan postbukannya pada common? Dari panduan keamanan rel :The solution to this is including a security token in non-GET requests
christianvuerings
29

The angular_rails_csrf permata otomatis menambahkan dukungan untuk pola yang dijelaskan dalam jawaban HungYuHei ini untuk semua pengendali Anda:

# Gemfile
gem 'angular_rails_csrf'
jsanders
sumber
tahu bagaimana Anda harus mengkonfigurasi pengontrol aplikasi Anda dan pengaturan terkait csrf / pemalsuan lainnya, untuk menggunakan angular_rails_csrf dengan benar?
Ben Wheeler
Pada saat komentar ini, angular_rails_csrfpermata tidak berfungsi dengan Rails 5. Namun, mengonfigurasi header permintaan Angular dengan nilai dari meta tag CSRF berfungsi!
bideowego
Ada rilis baru permata, yang mendukung Rails 5.
jsanders
4

Jawaban yang menggabungkan semua jawaban sebelumnya dan itu bergantung bahwa Anda menggunakan Devisepermata otentikasi.

Pertama-tama, tambahkan permata:

gem 'angular_rails_csrf'

Selanjutnya, tambahkan rescue_fromblok ke 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

Dan akhirnya, tambahkan modul interseptor ke aplikasi sudut Anda.

# 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
sumber
1
Mengapa Anda menyuntikkan, $injectorbukan hanya menyuntikkan langsung $http?
whitehat101
Ini berfungsi, tetapi hanya berpikir saya menambahkan memeriksa apakah permintaan sudah diulang. Ketika diulangi kami tidak mengirim lagi karena itu akan berulang selamanya.
duleorlovic
1

Saya melihat jawaban-jawaban lain dan berpikir mereka hebat dan dipikirkan dengan baik. Saya membuat aplikasi rel saya bekerja dengan apa yang saya pikir merupakan solusi yang lebih sederhana jadi saya pikir saya akan berbagi. Aplikasi rel saya datang dengan ini default di dalamnya,

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

Saya membaca komentar dan sepertinya itulah yang ingin saya gunakan angular dan menghindari kesalahan csrf. Saya mengubahnya menjadi ini,

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

Dan sekarang berhasil! Saya tidak melihat alasan mengapa ini tidak berhasil, tetapi saya ingin mendengar beberapa wawasan dari poster lain.

Blaine Hatab
sumber
6
ini akan menimbulkan masalah jika Anda mencoba menggunakan 'sesi' rel karena akan ditetapkan ke nol jika gagal dalam tes pemalsuan, yang akan selalu, karena Anda tidak mengirim token csrf dari sisi klien.
hajpoj
Tetapi jika Anda tidak menggunakan sesi Rails semuanya baik-baik saja; Terima kasih! Saya telah berjuang untuk menemukan solusi terbersih untuk ini.
Morgan
1

Saya telah menggunakan konten dari jawaban HungYuHei di aplikasi saya. Saya menemukan bahwa saya berurusan dengan beberapa masalah tambahan, beberapa karena penggunaan Devise untuk otentikasi, dan beberapa karena default yang saya dapatkan dengan aplikasi saya:

protect_from_forgery with: :exception

Saya perhatikan pertanyaan stack overflow terkait dan jawabannya di sana , dan saya menulis posting blog yang jauh lebih verbose yang merangkum berbagai pertimbangan. Bagian dari solusi yang relevan di sini adalah, di pengontrol aplikasi:

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

Saya menemukan peretasan yang sangat cepat untuk ini. Yang harus saya lakukan adalah sebagai berikut:

Sebuah. Dalam pandangan saya, saya menginisialisasi $scopevariabel yang berisi token, katakanlah sebelum formulir, atau bahkan lebih baik pada inisialisasi pengontrol:

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

b. Di pengontrol AngularJS saya, sebelum menyimpan entri baru saya, saya menambahkan token ke hash:

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

Tidak ada lagi yang perlu dilakukan.

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

Ini bekerja di sisi sudut!

Evgeniy Krokhmal
sumber