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