Rails CSRF Protection + Angular.js: protect_from_forgery ทำให้ฉันออกจากระบบ POST


129

หากprotect_from_forgeryมีการกล่าวถึงตัวเลือกใน application_controller ฉันสามารถเข้าสู่ระบบและดำเนินการตามคำขอ GET ใด ๆ ได้ แต่ในคำขอ POST แรก Rails จะรีเซ็ตเซสชันซึ่งจะทำให้ฉันไม่สามารถใช้งานได้

ฉันprotect_from_forgeryปิดตัวเลือกนี้ชั่วคราว แต่ต้องการใช้กับ Angular.js มีวิธีทำบ้างไหม


ดูว่าสิ่งนี้ช่วยได้หรือไม่เกี่ยวกับการตั้งค่าส่วนหัว HTTP stackoverflow.com/questions/14183025/…
Mark Rajcok

คำตอบ:


276

ฉันคิดว่าการอ่านค่า CSRF จาก DOM ไม่ใช่วิธีแก้ปัญหาที่ดี แต่เป็นวิธีแก้ปัญหาชั่วคราว

นี่คือรูปแบบเอกสารเว็บไซต์ทางการของ angularJS http://docs.angularjs.org/api/ng.$http :

เนื่องจากมีเพียง JavaScript ที่ทำงานบนโดเมนของคุณเท่านั้นที่สามารถอ่านคุกกี้ได้เซิร์ฟเวอร์ของคุณจึงมั่นใจได้ว่า XHR มาจาก JavaScript ที่ทำงานบนโดเมนของคุณ

เพื่อใช้ประโยชน์จากสิ่งนี้ (การป้องกัน CSRF) เซิร์ฟเวอร์ของคุณต้องตั้งค่าโทเค็นในคุกกี้เซสชันที่อ่านได้ของ JavaScript ที่เรียกว่า XSRF-TOKEN ในคำขอ HTTP GET แรก ในการร้องขอที่ไม่ใช่ GET ในภายหลังเซิร์ฟเวอร์สามารถตรวจสอบได้ว่าคุกกี้ตรงกับส่วนหัว HTTP X-XSRF-TOKEN

นี่คือวิธีแก้ปัญหาของฉันตามคำแนะนำเหล่านี้:

ขั้นแรกตั้งค่าคุกกี้:

# 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

จากนั้นเราควรตรวจสอบโทเค็นในทุกคำขอที่ไม่ใช่ GET
เนื่องจาก Rails ได้สร้างขึ้นด้วยวิธีการที่คล้ายกันแล้วเราจึงสามารถแทนที่มันเพื่อต่อท้ายตรรกะของเรา:

# 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
ฉันชอบเทคนิคนี้เพราะคุณไม่ต้องแก้ไขโค้ดฝั่งไคลเอ็นต์
Michelle Tilley

11
โซลูชันนี้รักษาประโยชน์ของการป้องกัน CSRF อย่างไร ด้วยการตั้งค่าคุกกี้เบราว์เซอร์ของผู้ใช้ที่ทำเครื่องหมายจะส่งคุกกี้นั้นในคำขอที่ตามมาทั้งหมดรวมถึงคำขอข้ามไซต์ ฉันสามารถตั้งค่าไซต์ของบุคคลที่สามที่เป็นอันตรายซึ่งส่งคำขอที่เป็นอันตรายและเบราว์เซอร์ของผู้ใช้จะส่ง 'XSRF-TOKEN' ไปยังเซิร์ฟเวอร์ ดูเหมือนว่าโซลูชันนี้จะเทียบเท่ากับการปิดการป้องกัน CSRF โดยสิ้นเชิง
Steven

9
จากเอกสาร Angular: "เนื่องจากมีเพียง JavaScript ที่ทำงานบนโดเมนของคุณเท่านั้นที่สามารถอ่านคุกกี้ได้เซิร์ฟเวอร์ของคุณจึงมั่นใจได้ว่า XHR มาจาก JavaScript ที่ทำงานบนโดเมนของคุณ" @StevenXu - เว็บไซต์ของบุคคลที่สามจะอ่านคุกกี้ได้อย่างไร?
Jimmy Baker

8
@JimmyBaker: ใช่คุณพูดถูก ฉันได้ตรวจสอบเอกสารแล้ว แนวทางนี้เป็นไปตามแนวคิด ฉันสับสนการตั้งค่าของคุกกี้กับการตรวจสอบความถูกต้องโดยไม่ทราบว่า Angular นั้นเฟรมเวิร์กกำลังตั้งค่าส่วนหัวที่กำหนดเองตามมูลค่าของคุกกี้!
Steven

5
form_authenticity_token สร้างค่าใหม่ในการโทรแต่ละครั้งใน Rails 4.2 ดังนั้นสิ่งนี้จะไม่ทำงานอีกต่อไป
Dave

78

หากคุณใช้การป้องกัน Rails CSRF เริ่มต้น ( <%= csrf_meta_tags %>) คุณสามารถกำหนดค่าโมดูล Angular ของคุณได้ดังนี้:

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

หรือถ้าคุณไม่ได้ใช้ CoffeeScript (อะไรนะ!?):

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

หากต้องการคุณสามารถส่งส่วนหัวเฉพาะในคำขอที่ไม่ใช่ GET โดยมีสิ่งต่อไปนี้:

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
]

นอกจากนี้อย่าลืมตรวจสอบคำตอบของ HungYuHeiซึ่งครอบคลุมฐานทั้งหมดบนเซิร์ฟเวอร์มากกว่าไคลเอนต์


ให้ฉันอธิบาย เอกสารฐานเป็นธรรมดา HTML ไม่ .erb <%= csrf_meta_tags %>ดังนั้นฉันไม่สามารถใช้ ฉันคิดว่าควรจะมีเพียงพอที่จะกล่าวถึงprotect_from_forgeryเท่านั้น จะทำอย่างไร? เอกสารพื้นฐานต้องเป็น HTML ธรรมดา (ฉันอยู่ที่นี่ไม่ใช่คนเลือก)
Paul

3
เมื่อคุณใช้protect_from_forgeryสิ่งที่คุณกำลังพูดคือ "เมื่อโค้ด JavaScript ของฉันส่งคำขอ Ajax ฉันสัญญาว่าจะส่งX-CSRF-Tokenส่วนหัวที่สอดคล้องกับโทเค็น CSRF ปัจจุบัน" ในการรับโทเค็นนี้ Rails จะแทรกลงใน DOM ด้วย<%= csrf_meta_token %>และรับเนื้อหาของเมตาแท็กด้วย jQuery เมื่อใดก็ตามที่ส่งคำขอ Ajax (ไดรเวอร์ Rails 3 UJS เริ่มต้นจะทำสิ่งนี้ให้คุณ) หากคุณไม่ได้ใช้ ERB จะไม่มีวิธีรับโทเค็นปัจจุบันจาก Rails เข้าสู่หน้าและ / หรือ JavaScript - ดังนั้นคุณจึงไม่สามารถใช้protect_from_forgeryในลักษณะนี้ได้
Michelle Tilley

ขอบคุณสำหรับคำอธิบาย สิ่งที่ฉันคิดว่าในแอปพลิเคชันฝั่งเซิร์ฟเวอร์แบบคลาสสิกฝั่งไคลเอ็นต์ได้รับcsrf_meta_tagsทุกครั้งที่เซิร์ฟเวอร์สร้างการตอบกลับและทุกครั้งที่แท็กเหล่านี้แตกต่างจากแท็กก่อนหน้านี้ ดังนั้นแท็กเหล่านี้จึงไม่ซ้ำกันสำหรับแต่ละคำขอ คำถามคือ: แอปพลิเคชันรับแท็กเหล่านี้สำหรับคำขอ AJAX ได้อย่างไร (โดยไม่มีเชิงมุม)? ฉันใช้ protect_from_forgery กับคำขอ jQuery POST ไม่เคยใส่ใจตัวเองกับการรับโทเค็น CSRF นี้และมันก็ใช้งานได้ อย่างไร?
Paul

1
ไดรเวอร์ Rails UJS ใช้jQuery.ajaxPrefilterดังที่แสดงไว้ที่นี่: github.com/indirect/jquery-rails/blob/c1eb6ae/vendor/assets/…คุณสามารถอ่านไฟล์นี้และดูห่วงทั้งหมดที่ Rails กระโดดผ่านเพื่อให้มันทำงานได้ดีโดยไม่ต้อง กังวลเกี่ยวกับมัน
Michelle Tilley

@BrandonTilley มันจะไม่สมเหตุสมผลทำแค่นี้putและpostแทนที่จะทำcommon? จากคู่มือการรักษาความปลอดภัยราง :The solution to this is including a security token in non-GET requests
christianvuerings

29

angular_rails_csrfอัญมณีโดยอัตโนมัติเพิ่มการสนับสนุนสำหรับรูปแบบที่อธิบายไว้ในคำตอบของ HungYuHeiไปยังตัวควบคุมทั้งหมดของคุณ:

# Gemfile
gem 'angular_rails_csrf'

มีความคิดอย่างไรที่คุณควรกำหนดค่าตัวควบคุมแอปพลิเคชันของคุณและการตั้งค่าอื่น ๆ ที่เกี่ยวข้องกับ csrf / การปลอมแปลงเพื่อใช้ angular_rails_csrf อย่างถูกต้อง
Ben Wheeler

ในช่วงเวลาของความคิดเห็นนี้angular_rails_csrfอัญมณีใช้ไม่ได้กับ Rails 5 อย่างไรก็ตามการกำหนดค่าส่วนหัวคำขอเชิงมุมด้วยค่าจากเมตาแท็ก CSRF ใช้งานได้!
bideowego

มีการเปิดตัวอัญมณีใหม่ซึ่งรองรับ Rails 5
jsanders

4

คำตอบที่รวมคำตอบก่อนหน้านี้ทั้งหมดและขึ้นอยู่กับว่าคุณกำลังใช้Deviseอัญมณีการพิสูจน์ตัวตน

ก่อนอื่นให้เพิ่มอัญมณี:

gem 'angular_rails_csrf'

จากนั้นเพิ่มrescue_fromบล็อกใน 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

และในที่สุดเพิ่มโมดูลดักฟังให้กับแอปเชิงมุมของคุณ

# 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
ทำไมคุณถึงฉีด$injectorแทนที่จะฉีดโดยตรง$http?
whitehat101

ใช้งานได้ แต่คิดว่าฉันเพิ่มเท่านั้นคือตรวจสอบว่ามีการร้องขอซ้ำแล้วหรือไม่ เมื่อมันซ้ำเราจะไม่ส่งอีกเพราะมันจะวนซ้ำตลอดไป
duleorlovic

1

ฉันเห็นคำตอบอื่น ๆ และคิดว่าพวกเขายอดเยี่ยมและคิดมาอย่างดี ฉันทำให้แอปรางของฉันทำงานได้แม้ว่าสิ่งที่ฉันคิดว่าเป็นวิธีแก้ปัญหาที่ง่ายกว่าก็เลยคิดว่าจะแชร์ แอปรางของฉันมาพร้อมกับค่าเริ่มต้นนี้

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

ฉันอ่านความคิดเห็นและดูเหมือนว่านั่นคือสิ่งที่ฉันต้องการใช้เชิงมุมและหลีกเลี่ยงข้อผิดพลาด csrf ฉันเปลี่ยนเป็นสิ่งนี้

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

และตอนนี้มันใช้งานได้แล้ว! ฉันไม่เห็นเหตุผลใด ๆ ว่าทำไมมันถึงไม่ได้ผล แต่ฉันอยากได้ยินข้อมูลเชิงลึกจากผู้โพสต์คนอื่น ๆ


6
สิ่งนี้จะทำให้เกิดปัญหาหากคุณพยายามใช้ 'เซสชัน' ของรางเนื่องจากจะถูกตั้งค่าเป็นศูนย์หากไม่ผ่านการทดสอบการปลอมแปลงซึ่งจะเป็นเช่นนั้นเสมอเนื่องจากคุณไม่ได้ส่งโทเค็น csrf จากฝั่งไคลเอ็นต์
hajpoj

แต่ถ้าคุณไม่ได้ใช้เซสชัน Rails ก็ทำได้ดี ขอบคุณ! ฉันพยายามดิ้นรนเพื่อหาวิธีแก้ปัญหานี้ที่สะอาดที่สุด
Morgan

1

ฉันใช้เนื้อหาจากคำตอบของ HungYuHei ในใบสมัครของฉัน ฉันพบว่าฉันกำลังเผชิญกับปัญหาเพิ่มเติมเล็กน้อยอย่างไรก็ตามบางส่วนเป็นเพราะการใช้ Devise สำหรับการตรวจสอบสิทธิ์และบางส่วนเป็นเพราะค่าเริ่มต้นที่ฉันได้รับจากแอปพลิเคชันของฉัน:

protect_from_forgery with: :exception

ฉันสังเกตคำถามเกี่ยวกับสแตกล้นที่เกี่ยวข้องและคำตอบที่นั่นและฉันเขียนบล็อกโพสต์แบบละเอียดมากขึ้นซึ่งสรุปข้อควรพิจารณาต่างๆ ส่วนของโซลูชันที่เกี่ยวข้องมีอยู่ในตัวควบคุมแอปพลิเคชัน:

  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

ฉันพบการแฮ็กที่รวดเร็วมากสำหรับสิ่งนี้ สิ่งที่ฉันต้องทำมีดังต่อไปนี้:

ก. ในมุมมองของฉันฉันเริ่มต้น$scopeตัวแปรที่มีโทเค็นสมมติว่าก่อนฟอร์มหรือดีกว่าในการเริ่มต้นคอนโทรลเลอร์:

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

ข. ในคอนโทรลเลอร์ AngularJS ของฉันก่อนบันทึกรายการใหม่ฉันเพิ่มโทเค็นในแฮช:

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

ไม่มีอะไรต้องทำอีกแล้ว


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

มันทำงานในด้าน angularjs!

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.