From 42c34d985b1a6491b3e8a3955912be1339af0c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Thibout=C3=B4t?= Date: Sat, 1 Apr 2023 06:08:10 -0400 Subject: [PATCH 1/2] feat: Support custom CSRF token retrieval. --- lib/hanami/action/csrf_protection.rb | 54 +++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/hanami/action/csrf_protection.rb b/lib/hanami/action/csrf_protection.rb index c6d1d014..870ba8d5 100644 --- a/lib/hanami/action/csrf_protection.rb +++ b/lib/hanami/action/csrf_protection.rb @@ -14,7 +14,10 @@ class Action # # It stores a "challenge" token in session. For each "state changing request" # (eg. POST, PATCH etc..), we should send a special param: - # _csrf_token. + # _csrf_token which contain the "challenge" token. + # + # We can specify how to retreive the token from the request, by overriding #request_csrf_token. + # This is useful if we want to handle XMLHttpRequest (XHR) requests. # # If the param matches with the challenge token, the flow can continue. # Otherwise the application detects an attack attempt, it reset the session @@ -63,6 +66,21 @@ class Action # end # end # end + # + # @example Custom Token Retrieval + # module Web::Controllers::Books + # class Create < Web::Action + # def handle(*) + # # ... + # end + # + # private + # + # def request_csrf_token(req) + # req.get_header('X-CSRF-Token') + # end + # end + # end module CSRFProtection # Session and params key for CSRF token. # @@ -107,6 +125,34 @@ def set_csrf_token(_req, res) res.session[CSRF_TOKEN] ||= generate_csrf_token end + # Get CSRF Token from request. + # + # By default retreives the token from the request param _csrf_token. + # + # Override this method, for custom handling of the request token retrieval. + # + # @since 2.X.X + # + # @api private + # + # @example Custom Token Retrieval + # module Web::Controllers::Books + # class Create < Web::Action + # def handle(*) + # # ... + # end + # + # private + # + # def request_csrf_token(req) + # req.get_header('X-CSRF-Token') + # end + # end + # end + def request_csrf_token(req) + req.params[CSRF_TOKEN] + end + # Verify if CSRF token from params, matches the one stored in session. # If not, it raises an error. # @@ -131,14 +177,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 From 184fa47ec70d483a01ad232a48f7a68203c8b7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Thibout=C3=B4t?= Date: Mon, 3 Apr 2023 20:29:00 -0400 Subject: [PATCH 2/2] feat: CSRF token retrieval from header "X-CSRF-Token". --- lib/hanami/action/csrf_protection.rb | 49 +++---------------- .../hanami/action/csrf_protection_spec.rb | 40 +++++++++++++-- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/lib/hanami/action/csrf_protection.rb b/lib/hanami/action/csrf_protection.rb index 870ba8d5..024e9756 100644 --- a/lib/hanami/action/csrf_protection.rb +++ b/lib/hanami/action/csrf_protection.rb @@ -13,13 +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 which contain the "challenge" token. + # (eg. POST, PATCH etc..), we should send a special param + # _csrf_token or header X-CSRF-Token which contain the "challenge" + # token. # - # We can specify how to retreive the token from the request, by overriding #request_csrf_token. - # This is useful if we want to handle XMLHttpRequest (XHR) requests. - # - # 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. # @@ -66,21 +64,6 @@ class Action # end # end # end - # - # @example Custom Token Retrieval - # module Web::Controllers::Books - # class Create < Web::Action - # def handle(*) - # # ... - # end - # - # private - # - # def request_csrf_token(req) - # req.get_header('X-CSRF-Token') - # end - # end - # end module CSRFProtection # Session and params key for CSRF token. # @@ -125,32 +108,14 @@ def set_csrf_token(_req, res) res.session[CSRF_TOKEN] ||= generate_csrf_token end - # Get CSRF Token from request. + # Get CSRF Token in request. # - # By default retreives the token from the request param _csrf_token. - # - # Override this method, for custom handling of the request token retrieval. + # Retreives the CSRF token from the request param _csrf_token or the request header X-CSRF-Token. # # @since 2.X.X - # # @api private - # - # @example Custom Token Retrieval - # module Web::Controllers::Books - # class Create < Web::Action - # def handle(*) - # # ... - # end - # - # private - # - # def request_csrf_token(req) - # req.get_header('X-CSRF-Token') - # end - # end - # end def request_csrf_token(req) - req.params[CSRF_TOKEN] + req.params[CSRF_TOKEN] || req.get_header("HTTP_X_CSRF_TOKEN") end # Verify if CSRF token from params, matches the one stored in session. 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