Skip to content

Commit 56e22bd

Browse files
committed
Add Rails 7.1 CSRF token support
1 parent 1d9767b commit 56e22bd

File tree

3 files changed

+55
-45
lines changed

3 files changed

+55
-45
lines changed

History.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
- Drop unnecessary jruby.compat.version and RackConfig.getCompatVersion() API
55
- Drop JMS support
66
- update (bundled) rack to 2.2.17
7+
- Fix Rails 7.1 CSRF protection when working with `JavaServletStore` sessions
78

89
## 1.2.4 (UNRELEASED)
910

10-
- update (bundled) rack to 2.2.16
11+
- update (bundled) rack to 2.2.17
12+
- Fix Rails 7.1 CSRF protection when working with `JavaServletStore` sessions
1113

1214
## 1.2.3
1315

14-
- avoid warnings due usage of `File.exists?`
16+
- avoid warnings due to usage of `File.exists?`
1517
- Fix Rails 7.1 compatibility by ensuring active_support is required before railtie
1618
- Workaround logger require issues with concurrent-ruby 1.3.5 and older Rails versions
1719
- Workaround NameError frozen string literal issues with JRuby 9.3 and Rails 5.2/6.0

src/main/ruby/jruby/rack/session_store.rb

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# See the file LICENSE.txt for details.
66
#++
77

8-
require 'rack/session/abstract/id' unless defined?(::Rack::Session::Abstract::ID)
8+
require 'rack/session/abstract/id' unless defined?(::Rack::Session::Abstract::Persisted)
99

1010
module JRuby::Rack
1111
module Session
@@ -28,7 +28,7 @@ def method_missing(method, *args, &block)
2828
end
2929

3030
# Rack based SessionStore implementation but compatible with (older) AbstractStore.
31-
class SessionStore < ::Rack::Session::Abstract::ID
31+
class SessionStore < ::Rack::Session::Abstract::Persisted
3232

3333
ENV_SERVLET_SESSION_KEY = 'java.servlet_session'.freeze
3434
RAILS_SESSION_KEY = "__current_rails_session".freeze
@@ -37,13 +37,6 @@ def initialize(app, options={})
3737
super(app, options.merge!(:cookie_only => false, :defer => true))
3838
end
3939

40-
def context(env, app = @app)
41-
req = make_request env
42-
prepare_session(req)
43-
status, headers, body = app.call(req.env)
44-
commit_session(req, status, headers, body)
45-
end
46-
4740
# (public) servlet specific methods :
4841

4942
def get_servlet_session(env, create = false)
@@ -69,7 +62,7 @@ def get_servlet_session(env, create = false)
6962
servlet_session
7063
end
7164

72-
private # Rack::Session::Abstract::ID overrides :
65+
private # Rack::Session::Abstract::Persisted overrides :
7366

7467
def session_class
7568
::JRuby::Rack::Session::SessionHash
@@ -83,6 +76,7 @@ def generate_sid(secure = @sid_secure)
8376
nil # dummy method - no session id generation with servlet API
8477
end
8578

79+
# Alternative to overriding find_session(req)
8680
def load_session(req) # session_id arg for get_session alias
8781
session_id, session = false, {}
8882
if servlet_session = get_servlet_session(req.env)
@@ -122,40 +116,49 @@ def loaded_session?(session)
122116
! session.is_a?(::JRuby::Rack::Session::SessionHash) || session.loaded?
123117
end
124118

125-
def commit_session(req, status, headers, body)
126-
session = req.env[::Rack::RACK_SESSION]
127-
options = req.env[::Rack::RACK_SESSION_OPTIONS]
119+
# Overridden from Rack, removing support for deferral and unnecessary cookie support when using Java Servlet sessions.
120+
def commit_session(req, _res)
121+
session = req.get_header ::Rack::RACK_SESSION
122+
options = session.options
128123

129124
if options[:drop] || options[:renew]
130-
destroy_session(req.env, options[:id], options)
125+
delete_session(req, session.id, options)
131126
end
132127

133-
return [status, headers, body] if options[:drop] || options[:skip]
128+
return if options[:drop] || options[:skip]
134129

135130
if loaded_session?(session)
136-
session_id = session.respond_to?(:id=) ? session.id : options[:id]
137-
session_data = session.to_hash.delete_if { |_,v| v.nil? }
138-
unless set_session(req.env, session_id, session_data, options)
139-
req.env["rack.errors"].puts("WARNING #{self.class.name} failed to save session. Content dropped.")
131+
# Mirror behaviour of Rails ActionDispatch::Session::AbstractStore#commit_session for Rails 7.1+ compatibility
132+
commit_csrf_token(req, session)
133+
134+
session_id ||= session.id
135+
session_data = session.to_hash.delete_if { |k, v| v.nil? }
136+
137+
unless write_session(req, session_id, session_data, options)
138+
req.get_header(::Rack::RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
140139
end
141140
end
141+
end
142142

143-
[status, headers, body]
143+
def commit_csrf_token(req, session_hash)
144+
csrf_token = req.env[::ActionController::RequestForgeryProtection::CSRF_TOKEN] if defined?(::ActionController::RequestForgeryProtection::CSRF_TOKEN)
145+
session_hash[:_csrf_token] = csrf_token if csrf_token
144146
end
145147

146-
def set_session(env, session_id, hash, options)
147-
if session_id.nil? && hash.empty?
148-
destroy_session(env)
148+
def write_session(req, session_id, session_hash, _options)
149+
if session_id.nil? && session_hash.empty?
150+
delete_session(req)
149151
return true
150152
end
151-
if servlet_session = get_servlet_session(env, true)
153+
154+
if servlet_session = get_servlet_session(req.env, true)
152155
begin
153156
servlet_session.synchronized do
154157
keys = servlet_session.getAttributeNames
155-
keys.select { |key| ! hash.has_key?(key) }.each do |key|
158+
keys.select { |key| ! session_hash.has_key?(key) }.each do |key|
156159
servlet_session.removeAttribute(key)
157160
end
158-
hash.delete_if do |key,value|
161+
session_hash.delete_if do |key,value|
159162
if String === key
160163
case value
161164
when String, Numeric, true, false, nil
@@ -171,8 +174,8 @@ def set_session(env, session_id, hash, options)
171174
end
172175
end
173176
end
174-
if ! hash.empty?
175-
marshalled_string = Marshal.dump(hash)
177+
if ! session_hash.empty?
178+
marshalled_string = Marshal.dump(session_hash)
176179
marshalled_bytes = marshalled_string.to_java_bytes
177180
servlet_session.setAttribute(RAILS_SESSION_KEY, marshalled_bytes)
178181
elsif servlet_session.getAttribute(RAILS_SESSION_KEY)
@@ -188,9 +191,9 @@ def set_session(env, session_id, hash, options)
188191
end
189192
end
190193

191-
def destroy_session(env, session_id = nil, options = nil)
192-
# session_id and options arg defaults for destory alias
193-
(session = get_servlet_session(env)) && session.synchronized { session.invalidate }
194+
def delete_session(req, _session_id = nil, _options = nil)
195+
# session_id and options arg defaults for delete alias
196+
(session = get_servlet_session(req.env)) && session.synchronized { session.invalidate }
194197
rescue java.lang.IllegalStateException # if session already invalid
195198
nil
196199
end

src/spec/ruby/action_controller/session/java_servlet_store_spec.rb

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,10 @@
33
describe "ActionController::Session::JavaServletStore" do
44

55
before :all do
6-
76
require 'active_support'
87
require 'action_controller'
9-
begin # help Rails 3.0 up
10-
require 'action_dispatch/middleware/session/abstract_store'
11-
rescue LoadError
12-
end
13-
begin # a Rails 2.3 require
14-
require 'action_controller/session/abstract_store'
15-
rescue LoadError
16-
end
17-
18-
require 'jruby/rack/session_store'
19-
208
require 'action_controller/session/java_servlet_store'
9+
require 'jruby/rack/session_store'
2110
end
2211

2312
before :each do
@@ -283,6 +272,22 @@
283272
expect( new_session.send(:getAttribute, "_csrf_token") ).to_not be nil
284273
end
285274

275+
it "propagates rails csrf token to session during commit" do
276+
skip "Only runs on Rails 7.1+" unless defined? ::ActionController::RequestForgeryProtection::CSRF_TOKEN
277+
session = double_http_session
278+
@request.should_receive(:getSession).and_return(session)
279+
280+
@app.should_receive(:call) do |env|
281+
env['rack.session']['foo'] = 'bar'
282+
env[::ActionController::RequestForgeryProtection::CSRF_TOKEN] = 'some_token'
283+
end
284+
@session_store.call(@env)
285+
286+
# CSRF token propagated from env to underlying session
287+
expect( session.send(:getAttribute, '_csrf_token') ).to eq 'some_token'
288+
expect( session.send(:getAttribute, 'foo') ).to eq 'bar'
289+
end
290+
286291
it "handles the skip session option" do
287292
@request.should_receive(:getSession).with(false).and_return @session
288293
@session.should_not_receive(:setAttribute)

0 commit comments

Comments
 (0)