request_forgery_protection.rb 12.9 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
V
Vijay Dev 已提交
16 17
  # token with the token in the session. Only HTML and JavaScript requests are checked,
  # so this will not protect your XML API (presumably you'll have a different
18 19 20 21 22 23 24 25 26
  # authentication scheme there anyway).
  #
  # 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 已提交
27 28 29
  #
  # It's important to remember that XML or JSON requests are also affected and if
  # you're building an API you'll need something like:
30 31 32
  #
  #   class ApplicationController < ActionController::Base
  #     protect_from_forgery
33
  #     skip_before_action :verify_authenticity_token, if: :json_request?
34 35 36 37 38 39 40
  #
  #     protected
  #
  #     def json_request?
  #       request.format.json?
  #     end
  #   end
41 42
  #
  # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
43 44
  # which checks the token and resets the session if it doesn't match what was expected.
  # A call to this method is generated for new \Rails applications by default.
45 46 47
  #
  # 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 已提交
48
  # <tt>csrf_meta_tags</tt> in the HTML +head+.
49 50 51
  #
  # Learn more about CSRF attacks and securing your application in the
  # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
52
  module RequestForgeryProtection
53
    extend ActiveSupport::Concern
54

55
    include AbstractController::Helpers
56
    include AbstractController::Callbacks
57 58

    included do
59 60
      # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
      # sets it to <tt>:authenticity_token</tt> by default.
61 62
      config_accessor :request_forgery_protection_token
      self.request_forgery_protection_token ||= :authenticity_token
63

64 65 66 67
      # Holds the class which implements the request forgery protection.
      config_accessor :forgery_protection_strategy
      self.forgery_protection_strategy = nil

68
      # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
69 70
      config_accessor :allow_forgery_protection
      self.allow_forgery_protection = true if allow_forgery_protection.nil?
71

72 73 74 75
      # 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

76 77
      helper_method :form_authenticity_token
      helper_method :protect_against_forgery?
78
    end
79

80
    module ClassMethods
D
David Albert 已提交
81
      # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
82
      #
83 84 85 86
      #   class ApplicationController < ActionController::Base
      #     protect_from_forgery
      #   end
      #
D
David Heinemeier Hansson 已提交
87
      #   class FooController < ApplicationController
A
AvnerCohen 已提交
88
      #     protect_from_forgery except: :index
D
David Heinemeier Hansson 已提交
89
      #
90
      # You can disable CSRF protection on controller by skipping the verification before_action:
91
      #   skip_before_action :verify_authenticity_token
92
      #
D
David Heinemeier Hansson 已提交
93 94
      # Valid Options:
      #
95
      # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
96 97 98 99 100 101
      # * <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.
102
      def protect_from_forgery(options = {})
103
        self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
104
        self.request_forgery_protection_token ||= :authenticity_token
105
        prepend_before_action :verify_authenticity_token, options
106
        append_after_action :verify_same_origin_request
107
      end
108 109 110

      private

111
      def protection_method_class(name)
112 113 114 115 116 117 118
        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
119 120 121 122
      class NullSession
        def initialize(controller)
          @controller = controller
        end
123 124 125

        # This is the method that defines the application behavior when a request is found to be unverified.
        def handle_unverified_request
126
          request = @controller.request
127
          request.session = NullSessionHash.new(request.env)
128 129 130 131 132
          request.env['action_dispatch.request.flash_hash'] = nil
          request.env['rack.session.options'] = { skip: true }
          request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
        end

133 134
        protected

135
        class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
136 137 138
          def initialize(env)
            super(nil, env)
            @data = {}
139 140 141
            @loaded = true
          end

142 143 144
          # no-op
          def destroy; end

145 146 147 148 149 150 151
          def exists?
            true
          end
        end

        class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
          def self.build(request)
152 153 154
            key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
            host          = request.host
            secure        = request.ssl?
155

156
            new(key_generator, host, secure, options_for_env({}))
157 158 159 160 161 162 163 164
          end

          def write(*)
            # nothing
          end
        end
      end

165 166 167 168
      class ResetSession
        def initialize(controller)
          @controller = controller
        end
169 170

        def handle_unverified_request
171
          @controller.reset_session
172 173 174
        end
      end

175 176 177 178
      class Exception
        def initialize(controller)
          @controller = controller
        end
179 180 181 182 183

        def handle_unverified_request
          raise ActionController::InvalidAuthenticityToken
        end
      end
184 185 186
    end

    protected
187 188 189 190 191 192 193 194 195 196 197
      # 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
198
        mark_for_same_origin_verification!
199 200

        if !verified_request?
201 202 203
          if logger && log_warning_on_csrf_failure
            logger.warn "Can't verify CSRF token authenticity"
          end
204 205 206 207
          handle_unverified_request
        end
      end

208
      def handle_unverified_request
S
Santiago Pastorino 已提交
209
        forgery_protection_strategy.new(self).handle_unverified_request
210 211
      end

212
      #:nodoc:
213 214 215 216 217 218 219 220 221 222 223 224 225
      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
226
        end
227 228
      end

229 230 231 232 233
      # GET requests are checked for cross-origin JavaScript after rendering.
      def mark_for_same_origin_verification!
        @marked_for_same_origin_verification = request.get?
      end

234 235 236
      # 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?
237
        @marked_for_same_origin_verification ||= false
238 239 240 241 242 243 244
      end

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

245 246
      AUTHENTICITY_TOKEN_LENGTH = 32

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

P
Pratik Naik 已提交
258
      # Sets the token value for the current session.
259
      def form_authenticity_token
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
        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)
        return false if encoded_masked_token.nil? || encoded_masked_token.empty?

        begin
          masked_token = Base64.strict_decode64(encoded_masked_token)
        rescue ArgumentError # encoded_masked_token is invalid Base64
          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)
310
        ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
311 312 313 314 315 316 317 318 319
      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*')
320
      end
321

322 323 324 325 326
      # The form's authenticity parameter. Override to provide your own.
      def form_authenticity_param
        params[request_forgery_protection_token]
      end

327
      # Checks if the controller allows forgery protection.
328
      def protect_against_forgery?
329
        allow_forgery_protection
330
      end
331
  end
332
end