Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicitly fail on attempts to write into disabled sessions #42231

Merged
merged 1 commit into from
May 18, 2021

Conversation

byroot
Copy link
Member

@byroot byroot commented May 15, 2021

Until now config.session_store :disabled simply silently
discard the session hash at the end of the request.

By explicitly failing on writes, it can help discovering bugs
earlier.

Reads are still permitted.

I'm opening this as a draft because there are still quite a lot of unknowns.

cc @pixeltrix @matthewd

Comment on lines 29 to 34
rescue ActionDispatch::Request::Session::DisabledSessionError
# TODO: proper deprecation message
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Calling `csrf_meta_tags` when session are disabled is deprecated. Either configure session or disable CSRF protection
MSG
nil
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good example of why failing loudly would be preferable. Right now if you disable sessions but forget to disable CSRF protection, the tokens would still be generated but would always fail to validate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another possibility is to not output any tags and log a debug warning.

This has echos of when we disabled autoload for the lib folder. Some people prefer hard failure and some people prefer soft warnings so I suspect it'll need to be configurable - hard failure for new applications and soft warnings for upgrading apps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another possibility is to not output any tags and log a debug warning.

Yes that's what I'm doing here as to preserve the current behavior (except it uses a deprecation warning rather than a regular one). I think that make sense when changing existing things.

hard failure for new applications and soft warnings for upgrading apps.

Agreed.

@byroot
Copy link
Member Author

byroot commented May 15, 2021

The Active Record test failure is also interesting:

Error:
ActiveRecord::DatabaseSelectorTest#test_the_middleware_chooses_writing_role_with_POST_request:
ActionDispatch::Request::Session::DisabledSessionError: Your application has sessions disabled. To write to the session you must first configure a session store
    /rails/actionpack/lib/action_dispatch/request/session.rb:250:in `load_for_write!'
    /rails/actionpack/lib/action_dispatch/request/session.rb:148:in `[]='
    /rails/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb:39:in `update_last_write_timestamp'
    /rails/activerecord/lib/active_record/middleware/database_selector/resolver.rb:73:in `block (2 levels) in write_to_primary'
    /rails/activesupport/lib/active_support/notifications/instrumenter.rb:44:in `instrument'
    /rails/activerecord/lib/active_record/middleware/database_selector/resolver.rb:70:in `block in write_to_primary'
    /rails/activerecord/lib/active_record/connection_handling.rb:370:in `with_role_and_shard'
    /rails/activerecord/lib/active_record/connection_handling.rb:160:in `connected_to'
    /rails/activerecord/lib/active_record/middleware/database_selector/resolver.rb:69:in `write_to_primary'
    /rails/activerecord/lib/active_record/middleware/database_selector/resolver.rb:44:in `write'
    /rails/activerecord/lib/active_record/middleware/database_selector.rb:70:in `select_database'
    /rails/activerecord/lib/active_record/middleware/database_selector.rb:57:in `call'
    /rails/activerecord/test/cases/database_selector_test.rb:285:in `test_the_middleware_chooses_writing_role_with_POST_request'

I think it makes sense. That's another footgun where you could setup the DB selection middleware in an API only app, and not realize that it's silently not working.

@pixeltrix
Copy link
Contributor

I think it makes sense. That's another footgun where you could setup the DB selection middleware in an API only app, and not realize that it's silently not working.

Ugh, seems that's a current issue - was surprising to me that the connection switching relies on the session.

Also we write the config out to production.rb even if the app is generated with --api - possibly should skip that though you can write your own custom resolver.

@byroot
Copy link
Member Author

byroot commented May 15, 2021

Ok, so I got the main CI green. I'd appreciate some more feedback on the current approach, and if it's positive I can try to get this PR to mergeable quality.

@pixeltrix
Copy link
Contributor

Ok, so I got the main CI green. I'd appreciate some more feedback on the current approach, and if it's positive I can try to get this PR to mergeable quality.

General approach seems fine but not keen on using exceptions as flow control - is this a temporary thing or is there issues in doing something else?

@byroot
Copy link
Member Author

byroot commented May 15, 2021

using exceptions as flow control

I'm not sure what you are referring to. If you are talking about the two rescue DisabledSessionError they are just to "specialize" the error and provide a more helpful one (or to just issue a warning for legacy apps).

@byroot
Copy link
Member Author

byroot commented May 15, 2021

they are just to "specialize" the error and provide a more helpful one

I realized I was planing on doing that but didn't yet -_-.

But ok, I think I now see what you mean. You'd expect a more explicit session.enabled? precondition check?

@pixeltrix
Copy link
Contributor

But ok, I think I now see what you mean. You'd expect a more explicit session.enabled? precondition check?

Yes, exceptions are fine where we're terminating the render but where we're continuing to process the page it's generally slower (unless things have changed in recent ruby versions?).

@byroot
Copy link
Member Author

byroot commented May 15, 2021

Yes, exceptions are fine where we're terminating the render

Right, in that specific case we do terminate (unless you have the legacy option enabled). I chose this approach as IMHO it makes the code more straightforward, but I don't mind changing it for a call.

it's generally slower (unless things have changed in recent ruby versions?).

No, no changes, exception cost are still directly related to how deep the stack is. In this case it didn't concern me because we're in an error path, so it's not expected that you hit this path at all in production.

@casperisfine casperisfine force-pushed the disabled-session branch 2 times, most recently from 6258313 to 7802575 Compare May 17, 2021 09:31
tag("meta", name: "csrf-token", content: form_authenticity_token)
].join("\n").html_safe
if session.respond_to?(:enabled?) && !session.enabled?
if Rails.application.config.action_dispatch.silence_disabled_session_errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this Rails.application.config reach, but I'm unsure what a better alternative would be.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the config would be better defined on action_controller so we can add it to CONTROLLER_DELEGATES like how we handle request_forgery_protection_token? We could also add a session_enabled? or session_disabled? helper method similar to protect_from_forgery?.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, actually that makes me think we could do this check in protect_against_forgery?. no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that probably makes sense.

@casperisfine
Copy link
Contributor

Ok, I changed the rescue for precondition checks.

@casperisfine casperisfine force-pushed the disabled-session branch 2 times, most recently from 5bcf936 to 1885f30 Compare May 17, 2021 12:17
@byroot byroot marked this pull request as ready for review May 17, 2021 12:53
@byroot
Copy link
Member Author

byroot commented May 17, 2021

I think it might be good to go now.

attr_accessor :silence_disabled_session_errors
end
@silence_disabled_session_errors = true

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why here and not a config_accessor in RequestForgeryProtection?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that's where configs are stored by default. It also avoid eager loading RequestForgeryProtection if it's not used (at least I think, I admit I haven't double checked)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I wasn't being clear - we have an included block in RequestForgeryProtection here:

https://github.com/rails/rails/blob/main/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L66-L67

Since we're accessing the config in that module I think it's safe to assume that it's loaded at that point is it not?

Do we also need to have a block in railties configuration for setting the default in new apps vs. upgrading apps?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're accessing the config in that module I think it's safe to assume that it's loaded at that point is it not?

Hum, I don't think so. Because if you have load_defaults 7.0, then we set config.action_controller.silence..., which will cause the action controller Railtie to try to set ActionController::Base.silence....

Do we also need to have a block in railties configuration for setting the default in new apps vs. upgrading apps?

Right here: https://github.com/rails/rails/pull/42231/files#diff-9f26e965e7e2a6842bd6ea755924a621038bc9dd8d63cfa6b34092aa32ede02cR204-R206 no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, I don't think so. Because if you have load_defaults 7.0, then we set config.action_controller.silence..., which will cause the action controller Railtie to try to set ActionController::Base.silence....

My concern is that it's not a pattern that's used elsewhere in Action Pack and it's not in the module where it's used. There's a similar scenario with default_protect_from_forgery is there not? That's set in the load_defaults 5.2 block so something prevents that from blowing up. The default_protect_from_forgery config is applied using the :action_controller_base load hook so could we not use the same approach?

Right here: https://github.com/rails/rails/pull/42231/files#diff-9f26e965e7e2a6842bd6ea755924a621038bc9dd8d63cfa6b34092aa32ede02cR204-R206 no?

Sorry, obviously my 👀 are deteriorating with my advancing years 👴🏻

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we not use the same approach?

You are totally right, I missed this. I updated the PR.

obviously my 👀 are deteriorating with my advancing years 👴🏻

🤣

@@ -90,6 +101,11 @@ module RequestForgeryProtection
config_accessor :default_protect_from_forgery
self.default_protect_from_forgery = false

# Controls wether trying to use forgery protection without a working session store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo here - should be whether

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 .

Fixed.

if !session.respond_to?(:enabled?) || session.enabled?
true
else
if Base.silence_disabled_session_errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one more - this doesn't need to use Base as there's an instance accessor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Until now `config.session_store :disabled` simply silently
discard the session hash at the end of the request.

By explictly failing on writes, it can help discovering bugs
earlier.

Reads are still permitted.
Comment on lines +1 to +5
* Writing into a disabled session will now raise an error.

Previously when no session store was set, writing into the session would silently fail.

*Jean Boussier*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a changelog entry, since it's quite a big change.

@casperisfine
Copy link
Contributor

Thanks for the reviews @pixeltrix!

I'll likely merge tomorrow unless someone else chime in.

@byroot byroot merged commit 9dc032a into rails:main May 18, 2021
@byroot byroot deleted the disabled-session branch May 18, 2021 06:59
Tonkpils added a commit to ViewComponent/view_component that referenced this pull request May 18, 2021
Rails 7.0 deprecated writing to a session when the session is disabled
and defaults a session to a disabled session. rails/rails#42231

In the past, ActionDispatch::TestRequest.create would set the
TestSession explicitly but this is not the case anymore and the default
session is used. With the PR mentioned above, this raises an exception
when testsing view components that implement sessions.
@@ -447,6 +441,10 @@ def check_method(name)
HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
name
end

def default_session
Session.disabled(self)
Copy link
Contributor

@Tonkpils Tonkpils May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this default_session as disabled without checking the configuration causes any requests built without a session to have this by default and can be surprising.

If my application has a config.session_store set as :cookie_store and I create an ActionDispatch::Request object its default session will be disabled. Would it be worth adding this information to the changelog as it is likely to break applications using ActionDispatch::Request manually

/cc @byroot

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We happened to run into this in our test suite from both ActionDispatch::TestRequest and when using ActionController::Renderer from a job. I do like the new behaviour, it could catch bugs where one tries to set a session which has no effect, but I think we should call out this change more explicitly as others are likely to run into it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any requests built without a session to have this by default and can be surprising.

But that's the idea no. Previously if you were to write into that request's session it would be silently not be persisted, right?

Also I'm curious to hear more about the use case of using ActionDispatch::Request manually.

Would it be worth adding this information to the changelog

I'm totally open to improve the changelog, if you have specific suggestions of things to call out, please let me know.

We happened to run into this in our test suite from both ActionDispatch::TestRequest

Hum, TestRequest should probably assume a working session no? That might be an oversight.

carlosantoniodasilva added a commit to heartcombo/devise that referenced this pull request Oct 7, 2021
It appears setting the `rack.session` to a simple hash doesn't work
anymore as it now has a few additional methods Rails is relying on to
determine whether it's enabled or not:
rails/rails#42231

Failure:
    NoMethodError: undefined method `enabled?' for {}:Hash
    rails (f55cdafe4b82) actionpack/lib/action_dispatch/middleware/flash.rb:62:in `commit_flash'

Turns we we don't seem to need to set `rack.session` for the tests here.
carlosantoniodasilva added a commit to heartcombo/devise that referenced this pull request Oct 7, 2021
It appears setting the `rack.session` to a simple hash doesn't work
anymore as it now has a few additional methods Rails is relying on to
determine whether it's enabled or not:
rails/rails#42231

Failure:
    NoMethodError: undefined method `enabled?' for {}:Hash
    rails (f55cdafe4b82) actionpack/lib/action_dispatch/middleware/flash.rb:62:in `commit_flash'

Turns we we don't seem to need to set `rack.session` for the tests here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants