-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Conversation
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
The Active Record test failure is also interesting:
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 |
1045c6e
to
bef3e60
Compare
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? |
I'm not sure what you are referring to. If you are talking about the two |
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 |
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?). |
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.
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. |
6258313
to
7802575
Compare
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Ok, I changed the |
5bcf936
to
1885f30
Compare
I think it might be good to go now. |
attr_accessor :silence_disabled_session_errors | ||
end | ||
@silence_disabled_session_errors = true | ||
|
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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:
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 setconfig.action_controller.silence...
, which will cause the action controller Railtie to try to setActionController::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 👴🏻
There was a problem hiding this comment.
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 👴🏻
🤣
1885f30
to
e9a9533
Compare
@@ -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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦 .
Fixed.
e9a9533
to
343d602
Compare
if !session.respond_to?(:enabled?) || session.enabled? | ||
true | ||
else | ||
if Base.silence_disabled_session_errors |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
343d602
to
d12f836
Compare
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.
d12f836
to
c1c96a0
Compare
* 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* |
There was a problem hiding this comment.
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.
Thanks for the reviews @pixeltrix! I'll likely merge tomorrow unless someone else chime in. |
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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.
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.
Until now
config.session_store :disabled
simply silentlydiscard 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