# Standalone copy of Hanami::Action::CsrfProtection from Hanami gem, copied from # https://raw.githubusercontent.com/hanami/hanami/unstable/lib/hanami/action/csrf_protection.rb # # This makes it possible for us to include CSRF protection without having to # require the "hanami" gem, which brings in a lot of unnecessary dependencies. require 'rack/utils' require 'securerandom' module Hanami # @api private class Action # Invalid CSRF Token # # @since 0.4.0 class InvalidCSRFTokenError < ::StandardError end # CSRF Protection # # This security mechanism is enabled automatically if sessions are turned on. # # It stores a "challenge" token in session. For each "state changing request" # (eg. POST, PATCH etc..), we should send a special param: # _csrf_token. # # If the param matches with the challenge token, the flow can continue. # Otherwise the application detects an attack attempt, it reset the session # and Hanami::Action::InvalidCSRFTokenError is raised. # # We can specify a custom handling strategy, by overriding #handle_invalid_csrf_token. # # Form helper (#form_for) automatically sets a hidden field with the # correct token. A special view method (#csrf_token) is available in # case the form markup is manually crafted. # # We can disable this check on action basis, by overriding #verify_csrf_token?. # # @since 0.4.0 # # @see https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29 # @see https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet # # @example Custom Handling # module Web::Controllers::Books # class Create # include Web::Action # # def call(params) # # ... # end # # private # # def handle_invalid_csrf_token # Web::Logger.warn "CSRF attack: expected #{ session[:_csrf_token] }, was #{ params[:_csrf_token] }" # # manual handling # end # end # end # # @example Bypass Security Check # module Web::Controllers::Books # class Create # include Web::Action # # def call(params) # # ... # end # # private # # def verify_csrf_token? # false # end # end # end module CSRFProtection # Session and params key for CSRF token. # # This key is shared with hanami-controller and hanami-helpers # # @since 0.4.0 # @api private CSRF_TOKEN = :_csrf_token # Idempotent HTTP methods # # By default, the check isn't performed if the request method is included # in this list. # # @since 0.4.0 # @api private IDEMPOTENT_HTTP_METHODS = Hash[ 'GET' => true, 'HEAD' => true, 'TRACE' => true, 'OPTIONS' => true ].freeze # @since 0.4.0 # @api private # def self.included(action) # action.class_eval do # before :set_csrf_token, :verify_csrf_token # end unless Hanami.env?(:test) # end # NOTE: Modified to remove Hanami.env? check (we don't have a Hanami.env) def self.included(action) action.class_eval do before :set_csrf_token, :verify_csrf_token end end private # Set CSRF Token in session # # @since 0.4.0 # @api private def set_csrf_token(req, res) res.session[CSRF_TOKEN] ||= generate_csrf_token end # Verify if CSRF token from params, matches the one stored in session. # If not, it raises an error. # # Don't override this method. # # To bypass the security check, please override #verify_csrf_token?. # For custom handling of an attack, please override #handle_invalid_csrf_token. # # @since 0.4.0 # @api private def verify_csrf_token(req, res) handle_invalid_csrf_token(req, res) if invalid_csrf_token?(req, res) end # Verify if CSRF token from params, matches the one stored in session. # # Don't override this method. # # @since 0.4.0 # @api private def invalid_csrf_token?(req, res) return false unless verify_csrf_token?(req, res) missing_csrf_token?(req, res) || !::Rack::Utils.secure_compare(req.session[CSRF_TOKEN], req.params[CSRF_TOKEN]) end # Verify the CSRF token was passed in params. # # @api private def missing_csrf_token?(req, res) Hanami::Utils::Blank.blank?(req.params[CSRF_TOKEN]) end # Generates a random CSRF Token # # @since 0.4.0 # @api private def generate_csrf_token SecureRandom.hex(32) end # Decide if perform the check or not. # # Override and return false if you want to bypass security check. # # @since 0.4.0 # # @example # module Web::Controllers::Books # class Create # include Web::Action # # def call(params) # # ... # end # # private # # def verify_csrf_token? # false # end # end # end def verify_csrf_token?(req, res) !IDEMPOTENT_HTTP_METHODS[req.request_method] end # Handle CSRF attack. # # The default policy resets the session and raises an exception. # # Override this method, for custom handling. # # @raise [Hanami::Action::InvalidCSRFTokenError] # # @since 0.4.0 # # @example # module Web::Controllers::Books # class Create # include Web::Action # # def call(params) # # ... # end # # private # # def handle_invalid_csrf_token # # custom invalid CSRF management goes here # end # end # end def handle_invalid_csrf_token(req, res) res.session.clear raise InvalidCSRFTokenError.new end end end end