Rails CSRF Protection + Angular.js: Protect_from_forgery khiến tôi phải đăng xuất khỏi POST


129

Nếu protect_from_forgerytùy chọn được đề cập trong application_controll, thì tôi có thể đăng nhập và thực hiện bất kỳ yêu cầu GET nào, nhưng trên yêu cầu POST đầu tiên, Rails đặt lại phiên, nó sẽ đăng xuất tôi.

Tôi protect_from_forgerytạm thời tắt tùy chọn này, nhưng muốn sử dụng nó với Angular.js. Có cách nào để làm điều đó?


Xem điều này có giúp ích gì không, về việc thiết lập tiêu đề HTTP stackoverflow.com/questions/14183025/
Mark

Câu trả lời:


276

Tôi nghĩ rằng đọc giá trị CSRF từ DOM không phải là một giải pháp tốt, nó chỉ là một cách giải quyết.

Đây là một mẫu tài liệu trang web chính thức angularJS http://docs.angularjs.org/api/ng.$http :

Vì chỉ JavaScript chạy trên miền của bạn mới có thể đọc cookie, máy chủ của bạn có thể được đảm bảo rằng XHR đến từ JavaScript chạy trên miền của bạn.

Để tận dụng lợi thế này (Bảo vệ CSRF), máy chủ của bạn cần đặt mã thông báo trong cookie phiên có thể đọc được JavaScript có tên XSRF-TOKEN theo yêu cầu HTTP GET đầu tiên. Trong các yêu cầu không NHẬN tiếp theo, máy chủ có thể xác minh rằng cookie khớp với tiêu đề HTTP X-XSRF-TOKEN

Đây là giải pháp của tôi dựa trên những hướng dẫn sau:

Đầu tiên, đặt 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

Sau đó, chúng tôi nên xác minh mã thông báo trên mỗi yêu cầu không NHẬN.
Vì Rails đã được xây dựng với phương thức tương tự, chúng ta chỉ cần ghi đè lên nó để nối thêm logic của chúng ta:

# 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
Tôi thích kỹ thuật này, vì bạn không phải sửa đổi bất kỳ mã phía máy khách nào.
Michelle Tilley

11
Làm thế nào để giải pháp này bảo vệ tính hữu ích của bảo vệ CSRF? Bằng cách đặt cookie, trình duyệt được đánh dấu của người dùng sẽ gửi cookie đó cho tất cả các yêu cầu tiếp theo bao gồm các yêu cầu trên nhiều trang web. Tôi có thể thiết lập trang web bên thứ ba độc hại gửi yêu cầu độc hại và trình duyệt của người dùng sẽ gửi 'XSRF-TOKEN' đến máy chủ. Có vẻ như giải pháp này tương đương với việc tắt bảo vệ CSRF hoàn toàn.
Steven

9
Từ các tài liệu Angular: "Vì chỉ JavaScript chạy trên miền của bạn mới có thể đọc cookie, nên máy chủ của bạn có thể được đảm bảo rằng XHR đến từ JavaScript chạy trên miền của bạn." @StevenXu - Trang web bên thứ ba sẽ đọc cookie như thế nào?
Jimmy Baker

8
@JimmyBaker: vâng, bạn đúng. Tôi đã xem lại tài liệu. Cách tiếp cận là âm thanh khái niệm. Tôi đã nhầm lẫn giữa cài đặt cookie với xác thực, không nhận ra rằng Angular khung đang đặt tiêu đề tùy chỉnh dựa trên giá trị của cookie!
Steven

5
form_authenticity_token tạo ra các giá trị mới cho mỗi cuộc gọi trong Rails 4.2, do đó, điều này dường như không còn hoạt động nữa.
Dave

78

Nếu bạn đang sử dụng bảo vệ Rails CSRF mặc định ( <%= csrf_meta_tags %>), bạn có thể định cấu hình mô-đun Angular của mình như sau:

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

Hoặc, nếu bạn không sử dụng CoffeeScript (cái gì!?):

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

Nếu bạn thích, bạn chỉ có thể gửi tiêu đề cho các yêu cầu không NHẬN với một số thứ như sau:

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
]

Ngoài ra, hãy chắc chắn kiểm tra câu trả lời của HungYuHei , bao gồm tất cả các cơ sở trên máy chủ thay vì máy khách.


Hãy để tôi giải thích. Tài liệu cơ sở là một HTML đơn giản, không phải .erb do đó tôi không thể sử dụng <%= csrf_meta_tags %>. Tôi nghĩ rằng chỉ nên có đủ để đề cập đến protect_from_forgery. Phải làm sao Tài liệu cơ sở phải là một HTML đơn giản (Tôi ở đây không phải là người chọn).
Paul

3
Khi bạn sử dụng protect_from_forgerynhững gì bạn đang nói là "khi mã JavaScript của tôi thực hiện các yêu cầu Ajax, tôi hứa sẽ gửi một X-CSRF-Tokentiêu đề tương ứng với mã thông báo CSRF hiện tại." Để có được mã thông báo này, Rails tiêm nó vào DOM với <%= csrf_meta_token %>và lấy nội dung của thẻ meta với jQuery bất cứ khi nào nó thực hiện các yêu cầu Ajax (trình điều khiển UJS Rails 3 mặc định thực hiện điều này cho bạn). Nếu bạn không sử dụng ERB, không có cách nào để nhận mã thông báo hiện tại từ Rails vào trang và / hoặc JavaScript - và do đó bạn không thể sử dụng protect_from_forgerytheo cách này.
Michelle Tilley

Cảm ơn bạn đã giải thích. Điều tôi nghĩ rằng trong một ứng dụng phía máy chủ cổ điển, phía máy khách nhận được csrf_meta_tagsmỗi lần máy chủ tạo phản hồi và mỗi lần các thẻ này khác với các thẻ trước đó. Vì vậy, các thẻ này là duy nhất cho mỗi yêu cầu. Câu hỏi là: làm thế nào để ứng dụng nhận được các thẻ này cho một yêu cầu AJAX (không có góc)? Tôi đã sử dụng bảo vệ_from_forgery với các yêu cầu POST của jQuery, không bao giờ làm phiền bản thân mình khi nhận mã thông báo CSRF này và nó đã hoạt động. Làm sao?
Paul

1
Người tài xế sử dụng Rails UJS jQuery.ajaxPrefilternhư ở đây: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/... Bạn có thể kiểm tra nội dung tập tin này và xem tất cả các hoops Rails nhảy qua để làm cho nó hoạt động khá nhiều mà không cần phải lo lắng về nó.
Michelle Tilley

@BrandonTilley sẽ không có ý nghĩa nếu chỉ làm điều này trên putpostthay vì trên common? Từ hướng dẫn bảo mật đường ray :The solution to this is including a security token in non-GET requests
christianvuerings

29

Các angular_rails_csrf đá quý tự động thêm hỗ trợ cho mô hình được mô tả trong câu trả lời của HungYuHei cho tất cả các bộ điều khiển của bạn:

# Gemfile
gem 'angular_rails_csrf'

Bất kỳ ý tưởng nào bạn nên định cấu hình bộ điều khiển ứng dụng của mình và các cài đặt liên quan đến csrf / giả mạo khác, để sử dụng angular_rails_csrf một cách chính xác?
Ben Wheeler

Tại thời điểm nhận xét này, angular_rails_csrfgem không hoạt động với Rails 5. Tuy nhiên, việc định cấu hình các tiêu đề yêu cầu Angular với giá trị từ thẻ meta CSRF hoạt động!
bideowego

Có một bản phát hành mới của đá quý, hỗ trợ Rails 5.
jsanders

4

Câu trả lời hợp nhất tất cả các câu trả lời trước đó và nó dựa vào việc bạn đang sử dụng Deviseđá quý xác thực.

Trước hết, thêm đá quý:

gem 'angular_rails_csrf'

Tiếp theo, thêm rescue_fromkhối vào application_controll.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

Và cuối cùng, thêm mô-đun chặn cho ứng dụng góc cạnh của bạn.

# 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
Tại sao bạn tiêm $injectorthay vì chỉ tiêm trực tiếp $http?
Whitehat101

Điều này hoạt động, nhưng chỉ nghĩ rằng tôi đã thêm là kiểm tra nếu yêu cầu đã được lặp lại. Khi nó được lặp lại, chúng tôi không gửi lại vì nó sẽ lặp lại mãi mãi.
duleorlovic

1

Tôi thấy những câu trả lời khác và nghĩ rằng chúng rất hay và được nghĩ ra. Tôi đã làm cho ứng dụng rails của mình hoạt động mặc dù với những gì tôi nghĩ là một giải pháp đơn giản hơn nên tôi nghĩ tôi muốn chia sẻ. Ứng dụng rails của tôi đi kèm với mặc định này trong đó,

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

Tôi đọc các bình luận và có vẻ như đó là những gì tôi muốn sử dụng góc cạnh và tránh lỗi csrf. Tôi đã thay đổi nó thành này,

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

Và bây giờ nó hoạt động! Tôi không thấy bất kỳ lý do tại sao điều này không hoạt động, nhưng tôi muốn nghe một số cái nhìn sâu sắc từ các áp phích khác.


6
điều này sẽ gây ra sự cố nếu bạn đang cố gắng sử dụng 'phiên' của đường ray vì nó sẽ được đặt thành không nếu nó không thực hiện kiểm tra giả mạo, điều này sẽ luôn luôn xảy ra, vì bạn không gửi mã thông báo csrf từ phía máy khách.
hajpoj

Nhưng nếu bạn không sử dụng phiên Rails thì tất cả đều ổn; cảm ơn bạn! Tôi đã phải vật lộn để tìm giải pháp sạch nhất cho vấn đề này.
Morgan

1

Tôi đã sử dụng nội dung từ câu trả lời của HungYuHei trong ứng dụng của mình. Tuy nhiên, tôi thấy rằng tôi đang xử lý một số vấn đề bổ sung, một số do tôi sử dụng Devise để xác thực và một số do mặc định mà tôi gặp phải với ứng dụng của mình:

protect_from_forgery with: :exception

Tôi lưu ý câu hỏi tràn ngăn xếp liên quan và các câu trả lời ở đó , và tôi đã viết một bài đăng blog dài dòng hơn nhiều để tóm tắt các cân nhắc khác nhau. Các phần của giải pháp có liên quan ở đây là, trong bộ điều khiển ứng dụng:

  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

Tôi tìm thấy một hack rất nhanh để này. Tất cả tôi phải làm là như sau:

a. Theo quan điểm của tôi, tôi khởi tạo một $scopebiến chứa mã thông báo, giả sử trước biểu mẫu hoặc thậm chí tốt hơn khi khởi tạo bộ điều khiển:

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

b. Trong bộ điều khiển AngularJS của tôi, trước khi lưu mục nhập mới của tôi, tôi thêm mã thông báo vào hàm băm:

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

Không cần phải làm gì thêm.


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

Nó hoạt động ở phía angularjs!

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.