diff --git a/lib/hanami/action/csrf_protection.rb b/lib/hanami/action/csrf_protection.rb index c6d1d014..024e9756 100644 --- a/lib/hanami/action/csrf_protection.rb +++ b/lib/hanami/action/csrf_protection.rb @@ -13,10 +13,11 @@ class Action # 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. + # (eg. POST, PATCH etc..), we should send a special param + # _csrf_token or header X-CSRF-Token which contain the "challenge" + # token. # - # If the param matches with the challenge token, the flow can continue. + # If the request token 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. # @@ -107,6 +108,16 @@ def set_csrf_token(_req, res) res.session[CSRF_TOKEN] ||= generate_csrf_token end + # Get CSRF Token in request. + # + # Retreives the CSRF token from the request param _csrf_token or the request header X-CSRF-Token. + # + # @since 2.X.X + # @api private + def request_csrf_token(req) + req.params[CSRF_TOKEN] || req.get_header("HTTP_X_CSRF_TOKEN") + end + # Verify if CSRF token from params, matches the one stored in session. # If not, it raises an error. # @@ -131,14 +142,14 @@ 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]) + !::Rack::Utils.secure_compare(req.session[CSRF_TOKEN], request_csrf_token(req)) end # Verify the CSRF token was passed in params. # # @api private - def missing_csrf_token?(req, *) - Hanami::Utils::Blank.blank?(req.params[CSRF_TOKEN]) + def missing_csrf_token?(req, res) + Hanami::Utils::Blank.blank?(request_csrf_token(req)) end # Generates a random CSRF Token diff --git a/spec/unit/hanami/action/csrf_protection_spec.rb b/spec/unit/hanami/action/csrf_protection_spec.rb index 80086429..91d3bee1 100644 --- a/spec/unit/hanami/action/csrf_protection_spec.rb +++ b/spec/unit/hanami/action/csrf_protection_spec.rb @@ -15,7 +15,7 @@ context "No existing session" do let(:request) { super().merge("rack.session" => {}) } - context "non-matching CSRF token in request" do + context "non-matching CSRF token in request param" do let(:request) { super().merge(_csrf_token: "non-matching") } it "rejects the request" do @@ -23,6 +23,14 @@ end end + context "non-matching CSRF token in request header" do + let(:request) { super().merge("HTTP_X_CSRF_TOKEN" => "non-matching") } + + it "rejects the request" do + expect { response }.to raise_error Hanami::Action::InvalidCSRFTokenError + end + end + context "missing CSRF token in request" do it "rejects the request" do expect { response }.to raise_error Hanami::Action::InvalidCSRFTokenError @@ -35,7 +43,7 @@ let(:session) { {_csrf_token: session_token} } let(:session_token) { "abc123" } - context "matching CSRF token in request" do + context "matching CSRF token in request param" do let(:request) { super().merge(_csrf_token: session_token) } it "accepts the request" do @@ -43,7 +51,15 @@ end end - context "non-matching CSRF token in request" do + context "matching CSRF token in request header" do + let(:request) { super().merge("HTTP_X_CSRF_TOKEN" => session_token) } + + it "accepts the request" do + expect(response.status).to eq 200 + end + end + + context "non-matching CSRF token in request param" do let(:request) { super().merge(_csrf_token: "non-matching") } it "rejects the request" do @@ -51,6 +67,14 @@ end end + context "non-matching CSRF token in request header" do + let(:request) { super().merge("HTTP_X_CSRF_TOKEN" => "non-matching") } + + it "rejects the request" do + expect { response }.to raise_error Hanami::Action::InvalidCSRFTokenError + end + end + context "missing CSRF token in request" do it "rejects the request" do expect { response }.to raise_error Hanami::Action::InvalidCSRFTokenError @@ -74,13 +98,21 @@ def verify_csrf_token?(_req, _res) end end - context "non-matching CSRF token in request" do + context "non-matching CSRF token in request param" do let(:request) { super().merge(_csrf_token: "non-matching") } it "accepts the request" do expect(response.status).to eq 200 end end + + context "non-matching CSRF token in request header" do + let(:request) { super().merge("HTTP_X_CSRF_TOKEN" => "non-matching") } + + it "accepts the request" do + expect(response.status).to eq 200 + end + end end end end