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
D
David Heinemeier Hansson 已提交
93
      #
94
      # You can disable forgery protection on controller by skipping the verification before_action:
95
      #   skip_before_action :verify_authenticity_token
96
      #
D
David Heinemeier Hansson 已提交
97 98
      # Valid Options:
      #
99
      # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>.
100
      # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
101 102
      # * <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
103
      #   (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the
104 105
      #   verification callback in the position of the protect_from_forgery call. This means any callbacks added
      #   before are run first.
106 107 108 109 110 111
      # * <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.
112
      def protect_from_forgery(options = {})
113 114
        options = options.reverse_merge(prepend: true)

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

      private

123
      def protection_method_class(name)
124 125 126 127 128 129 130
        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
131 132 133 134
      class NullSession
        def initialize(controller)
          @controller = controller
        end
135 136 137

        # This is the method that defines the application behavior when a request is found to be unverified.
        def handle_unverified_request
138
          request = @controller.request
139
          request.session = NullSessionHash.new(request.env)
140 141
          request.env['action_dispatch.request.flash_hash'] = nil
          request.env['rack.session.options'] = { skip: true }
A
Aaron Patterson 已提交
142
          request.env['action_dispatch.cookies'] = NullCookieJar.build(request, {})
143 144
        end

145 146
        protected

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

154 155 156
          # no-op
          def destroy; end

157 158 159 160 161 162 163 164 165 166 167 168
          def exists?
            true
          end
        end

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

169 170 171 172
      class ResetSession
        def initialize(controller)
          @controller = controller
        end
173 174

        def handle_unverified_request
175
          @controller.reset_session
176 177 178
        end
      end

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

        def handle_unverified_request
          raise ActionController::InvalidAuthenticityToken
        end
      end
188 189 190
    end

    protected
191 192 193 194 195 196 197 198 199 200 201
      # 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
202
        mark_for_same_origin_verification!
203 204

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

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

216
      #:nodoc:
217 218 219 220 221 222 223 224 225 226 227 228 229
      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
230
        end
231 232
      end

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

238 239 240
      # 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?
241
        @marked_for_same_origin_verification ||= false
242 243 244 245 246 247 248
      end

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

249 250
      AUTHENTICITY_TOKEN_LENGTH = 32

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

P
Pratik Naik 已提交
262
      # Sets the token value for the current session.
263
      def form_authenticity_token
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
        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)
281 282 283
        if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
          return false
        end
284 285 286

        begin
          masked_token = Base64.strict_decode64(encoded_masked_token)
287
        rescue ArgumentError # encoded_masked_token is invalid Base64
288 289 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
          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)
316
        ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
317 318 319 320 321 322 323 324 325
      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*')
326
      end
327

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

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