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
  #
  #   class ApplicationController < ActionController::Base
32
  #     protect_from_forgery unless: -> { request.format.json? }
33
  #   end
34 35
  #
  # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
36 37
  # 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.
38 39 40
  #
  # 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 已提交
41
  # <tt>csrf_meta_tags</tt> in the HTML +head+.
42 43 44
  #
  # Learn more about CSRF attacks and securing your application in the
  # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
45
  module RequestForgeryProtection
46
    extend ActiveSupport::Concern
47

48
    include AbstractController::Helpers
49
    include AbstractController::Callbacks
50 51

    included do
52 53
      # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
      # sets it to <tt>:authenticity_token</tt> by default.
54 55
      config_accessor :request_forgery_protection_token
      self.request_forgery_protection_token ||= :authenticity_token
56

57 58 59 60
      # Holds the class which implements the request forgery protection.
      config_accessor :forgery_protection_strategy
      self.forgery_protection_strategy = nil

61
      # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
62 63
      config_accessor :allow_forgery_protection
      self.allow_forgery_protection = true if allow_forgery_protection.nil?
64

65 66 67 68
      # 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

69 70
      helper_method :form_authenticity_token
      helper_method :protect_against_forgery?
71
    end
72

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

      private

105
      def protection_method_class(name)
106 107 108 109 110 111 112
        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
113 114 115 116
      class NullSession
        def initialize(controller)
          @controller = controller
        end
117 118 119

        # This is the method that defines the application behavior when a request is found to be unverified.
        def handle_unverified_request
120
          request = @controller.request
121
          request.session = NullSessionHash.new(request.env)
122 123 124 125 126
          request.env['action_dispatch.request.flash_hash'] = nil
          request.env['rack.session.options'] = { skip: true }
          request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
        end

127 128
        protected

129
        class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
130 131 132
          def initialize(env)
            super(nil, env)
            @data = {}
133 134 135
            @loaded = true
          end

136 137 138
          # no-op
          def destroy; end

139 140 141 142 143 144 145
          def exists?
            true
          end
        end

        class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
          def self.build(request)
146 147 148
            key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
            host          = request.host
            secure        = request.ssl?
149

150
            new(key_generator, host, secure, options_for_env({}))
151 152 153 154 155 156 157 158
          end

          def write(*)
            # nothing
          end
        end
      end

159 160 161 162
      class ResetSession
        def initialize(controller)
          @controller = controller
        end
163 164

        def handle_unverified_request
165
          @controller.reset_session
166 167 168
        end
      end

169 170 171 172
      class Exception
        def initialize(controller)
          @controller = controller
        end
173 174 175 176 177

        def handle_unverified_request
          raise ActionController::InvalidAuthenticityToken
        end
      end
178 179 180
    end

    protected
181 182 183 184 185 186 187 188 189 190 191
      # 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
192
        mark_for_same_origin_verification!
193 194

        if !verified_request?
195 196 197
          if logger && log_warning_on_csrf_failure
            logger.warn "Can't verify CSRF token authenticity"
          end
198 199 200 201
          handle_unverified_request
        end
      end

202
      def handle_unverified_request
S
Santiago Pastorino 已提交
203
        forgery_protection_strategy.new(self).handle_unverified_request
204 205
      end

206
      #:nodoc:
207 208 209 210 211 212 213 214 215 216 217 218 219
      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
220
        end
221 222
      end

223 224 225 226 227
      # GET requests are checked for cross-origin JavaScript after rendering.
      def mark_for_same_origin_verification!
        @marked_for_same_origin_verification = request.get?
      end

228 229 230
      # 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?
231
        @marked_for_same_origin_verification ||= false
232 233 234 235 236 237 238
      end

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

239 240
      AUTHENTICITY_TOKEN_LENGTH = 32

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

P
Pratik Naik 已提交
252
      # Sets the token value for the current session.
253
      def form_authenticity_token
254 255 256 257 258 259 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
        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)
304
        ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
305 306 307 308 309 310 311 312 313
      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*')
314
      end
315

316 317 318 319 320
      # The form's authenticity parameter. Override to provide your own.
      def form_authenticity_param
        params[request_forgery_protection_token]
      end

321
      # Checks if the controller allows forgery protection.
322
      def protect_against_forgery?
323
        allow_forgery_protection
324
      end
325
  end
326
end