request_forgery_protection.rb 13.8 KB
Newer Older
A
Arun Agrawal 已提交
1
require 'rack/session/abstract/id'
2
require 'action_controller/metal/exceptions'
3
require 'active_support/security_utils'
J
Jeremy Kemper 已提交
4

5
module ActionController #:nodoc:
6 7
  class InvalidAuthenticityToken < ActionControllerError #:nodoc:
  end
8

9 10 11
  class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
  end

12
  # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
H
Hendy Tanata 已提交
13
  # by including a token in the rendered HTML for your application. This token is
14
  # stored as a random string in the session, to which an attacker does not have
15
  # access. When a request reaches your application, \Rails verifies the received
16 17
  # token with the token in the session. All requests are checked except GET requests
  # as these should be idempotent. Keep in mind that all session-oriented requests
18
  # should be CSRF protected, including JavaScript and HTML requests.
19
  #
20
  # Since HTML and JavaScript requests are typically made from the browser, we
21
  # need to ensure to verify request authenticity for the web browser. We can
22
  # use session-oriented authentication for these types of requests, by using
23
  # the `protect_from_forgery` method in our controllers.
24
  #
25 26 27 28 29 30 31
  # GET requests are not protected since they don't have side effects like writing
  # to the database and don't leak sensitive information. JavaScript requests are
  # an exception: a third-party site can use a <script> tag to reference a JavaScript
  # URL on your site. When your JavaScript response loads on their site, it executes.
  # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
  # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
  # Ajax) requests are allowed to make GET requests for JavaScript responses.
V
Vijay Dev 已提交
32 33
  #
  # It's important to remember that XML or JSON requests are also affected and if
34 35
  # you're building an API you should change forgery protection method in
  # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
36 37
  #
  #   class ApplicationController < ActionController::Base
38
  #     protect_from_forgery unless: -> { request.format.json? }
39
  #   end
40
  #
41 42
  # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
  # By default <tt>protect_from_forgery</tt> protects your session with
43 44
  # <tt>:null_session</tt> method, which provides an empty session
  # during request.
45
  #
46
  # We may want to disable CSRF protection for APIs since they are typically
47
  # designed to be state-less. That is, the request API client will handle
48 49
  # the session for you instead of Rails.
  #
50 51
  # The token parameter is named <tt>authenticity_token</tt> by default. The name and
  # value of this token must be added to every layout that renders forms by including
H
Hendy Tanata 已提交
52
  # <tt>csrf_meta_tags</tt> in the HTML +head+.
53 54 55
  #
  # Learn more about CSRF attacks and securing your application in the
  # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
56
  module RequestForgeryProtection
57
    extend ActiveSupport::Concern
58

59
    include AbstractController::Helpers
60
    include AbstractController::Callbacks
61 62

    included do
63 64
      # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
      # sets it to <tt>:authenticity_token</tt> by default.
65 66
      config_accessor :request_forgery_protection_token
      self.request_forgery_protection_token ||= :authenticity_token
67

68 69 70 71
      # Holds the class which implements the request forgery protection.
      config_accessor :forgery_protection_strategy
      self.forgery_protection_strategy = nil

72
      # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
73 74
      config_accessor :allow_forgery_protection
      self.allow_forgery_protection = true if allow_forgery_protection.nil?
75

76 77 78 79
      # Controls whether a CSRF failure logs a warning. On by default.
      config_accessor :log_warning_on_csrf_failure
      self.log_warning_on_csrf_failure = true

80 81
      helper_method :form_authenticity_token
      helper_method :protect_against_forgery?
82
    end
83

84
    module ClassMethods
D
David Albert 已提交
85
      # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
86
      #
87 88 89 90
      #   class ApplicationController < ActionController::Base
      #     protect_from_forgery
      #   end
      #
D
David Heinemeier Hansson 已提交
91
      #   class FooController < ApplicationController
A
AvnerCohen 已提交
92
      #     protect_from_forgery except: :index
93
      #   end
D
David Heinemeier Hansson 已提交
94
      #
95
      # You can disable forgery protection on controller by skipping the verification before_action:
96
      #
97
      #   skip_before_action :verify_authenticity_token
98
      #
D
David Heinemeier Hansson 已提交
99 100
      # Valid Options:
      #
101
      # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>.
102
      # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
103 104
      # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the
      #   callback chain. If you need to make the verification depend on other callbacks, like authentication methods
105
      #   (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the
106 107
      #   verification callback in the position of the protect_from_forgery call. This means any callbacks added
      #   before are run first.
108 109 110 111 112 113
      # * <tt>:with</tt> - Set the method to handle unverified request.
      #
      # Valid unverified request handling methods are:
      # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
      # * <tt>:reset_session</tt> - Resets the session.
      # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
114
      def protect_from_forgery(options = {})
115 116
        options = options.reverse_merge(prepend: true)

117
        self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
118
        self.request_forgery_protection_token ||= :authenticity_token
119
        before_action :verify_authenticity_token, options
120
        append_after_action :verify_same_origin_request
121
      end
122 123 124

      private

125
      def protection_method_class(name)
126 127 128 129 130 131 132
        ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
      rescue NameError
        raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session'
      end
    end

    module ProtectionMethods
133 134 135 136
      class NullSession
        def initialize(controller)
          @controller = controller
        end
137 138 139

        # This is the method that defines the application behavior when a request is found to be unverified.
        def handle_unverified_request
140
          request = @controller.request
141
          request.session = NullSessionHash.new(request)
A
Aaron Patterson 已提交
142
          request.flash = nil
143
          request.session_options = { skip: true }
144
          request.cookie_jar = NullCookieJar.build(request, {})
145 146
        end

147 148
        protected

149
        class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
150 151
          def initialize(req)
            super(nil, req)
152
            @data = {}
153 154 155
            @loaded = true
          end

156 157 158
          # no-op
          def destroy; end

159 160 161 162 163 164 165 166 167 168 169 170
          def exists?
            true
          end
        end

        class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
          def write(*)
            # nothing
          end
        end
      end

171 172 173 174
      class ResetSession
        def initialize(controller)
          @controller = controller
        end
175 176

        def handle_unverified_request
177
          @controller.reset_session
178 179 180
        end
      end

181 182 183 184
      class Exception
        def initialize(controller)
          @controller = controller
        end
185 186 187 188 189

        def handle_unverified_request
          raise ActionController::InvalidAuthenticityToken
        end
      end
190 191 192
    end

    protected
193 194 195 196 197 198 199 200 201 202 203
      # The actual before_action that is used to verify the CSRF token.
      # Don't override this directly. Provide your own forgery protection
      # strategy instead. If you override, you'll disable same-origin
      # `<script>` verification.
      #
      # Lean on the protect_from_forgery declaration to mark which actions are
      # due for same-origin request verification. If protect_from_forgery is
      # enabled on an action, this before_action flags its after_action to
      # verify that JavaScript responses are for XHR requests, ensuring they
      # follow the browser's same-origin policy.
      def verify_authenticity_token
204
        mark_for_same_origin_verification!
205 206

        if !verified_request?
207 208 209
          if logger && log_warning_on_csrf_failure
            logger.warn "Can't verify CSRF token authenticity"
          end
210 211 212 213
          handle_unverified_request
        end
      end

214
      def handle_unverified_request
S
Santiago Pastorino 已提交
215
        forgery_protection_strategy.new(self).handle_unverified_request
216 217
      end

218
      #:nodoc:
219 220 221 222 223 224 225 226 227 228 229 230 231
      CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
        "<script> tag on another site requested protected JavaScript. " \
        "If you know what you're doing, go ahead and disable forgery " \
        "protection on this action to permit cross-origin JavaScript embedding."
      private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING

      # If `verify_authenticity_token` was run (indicating that we have
      # forgery protection enabled for this request) then also verify that
      # we aren't serving an unauthorized cross-origin response.
      def verify_same_origin_request
        if marked_for_same_origin_verification? && non_xhr_javascript_response?
          logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
          raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
232
        end
233 234
      end

235 236 237 238 239
      # GET requests are checked for cross-origin JavaScript after rendering.
      def mark_for_same_origin_verification!
        @marked_for_same_origin_verification = request.get?
      end

240 241 242
      # If the `verify_authenticity_token` before_action ran, verify that
      # JavaScript responses are only served to same-origin GET requests.
      def marked_for_same_origin_verification?
243
        @marked_for_same_origin_verification ||= false
244 245 246 247 248 249 250
      end

      # Check for cross-origin JavaScript responses.
      def non_xhr_javascript_response?
        content_type =~ %r(\Atext/javascript) && !request.xhr?
      end

251 252
      AUTHENTICITY_TOKEN_LENGTH = 32

253
      # Returns true or false if a request is verified. Checks:
254
      #
Y
yui-knk 已提交
255
      # * Is it a GET or HEAD request?  Gets should be safe and idempotent
256
      # * Does the form_authenticity_token match the given token value from the params?
257
      # * Does the X-CSRF-Token header match the form_authenticity_token
258
      def verified_request?
259
        !protect_against_forgery? || request.get? || request.head? ||
260 261
          valid_authenticity_token?(session, form_authenticity_param) ||
          valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
262
      end
Y
Yehuda Katz 已提交
263

P
Pratik Naik 已提交
264
      # Sets the token value for the current session.
265
      def form_authenticity_token
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
        masked_authenticity_token(session)
      end

      # Creates a masked version of the authenticity token that varies
      # on each request. The masking is used to mitigate SSL attacks
      # like BREACH.
      def masked_authenticity_token(session)
        one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
        encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
        masked_token = one_time_pad + encrypted_csrf_token
        Base64.strict_encode64(masked_token)
      end

      # Checks the client's masked token to see if it matches the
      # session token. Essentially the inverse of
      # +masked_authenticity_token+.
      def valid_authenticity_token?(session, encoded_masked_token)
283 284 285
        if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
          return false
        end
286 287 288

        begin
          masked_token = Base64.strict_decode64(encoded_masked_token)
289
        rescue ArgumentError # encoded_masked_token is invalid Base64
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
          return false
        end

        # See if it's actually a masked token or not. In order to
        # deploy this code, we should be able to handle any unmasked
        # tokens that we've issued without error.

        if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
          # This is actually an unmasked token. This is expected if
          # you have just upgraded to masked tokens, but should stop
          # happening shortly after installing this gem
          compare_with_real_token masked_token, session

        elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
          # Split the token into the one-time pad and the encrypted
          # value and decrypt it
          one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
          encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
          csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)

          compare_with_real_token csrf_token, session

        else
          false # Token is malformed
        end
      end

      def compare_with_real_token(token, session)
318
        ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
319 320 321 322 323 324 325 326 327
      end

      def real_csrf_token(session)
        session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
        Base64.strict_decode64(session[:_csrf_token])
      end

      def xor_byte_strings(s1, s2)
        s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
328
      end
329

330 331 332 333 334
      # The form's authenticity parameter. Override to provide your own.
      def form_authenticity_param
        params[request_forgery_protection_token]
      end

335
      # Checks if the controller allows forgery protection.
336
      def protect_against_forgery?
337
        allow_forgery_protection
338
      end
339
  end
340
end