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

Fix relation.create to avoid leaking scope to initialization block and callbacks #35186

Merged
merged 1 commit into from
Feb 7, 2019

Conversation

kamipo
Copy link
Member

@kamipo kamipo commented Feb 7, 2019

relation.create populates scope attributes to new record by scoping,
it is necessary to assign the scope attributes to the record and to find
STI subclass from the scope attributes.

But the effect of scoping is class global, it was caused undesired
behavior that pollute all class level querying methods in initialization
block and callbacks (after_initialize, before_validation,
before_save, etc), which are user provided code.

To avoid the leaking scope issue, restore the original current scope
before initialization block and callbacks are invoked.

Fixes #9894.
Fixes #17577.
Closes #31526.

…and callbacks

`relation.create` populates scope attributes to new record by `scoping`,
it is necessary to assign the scope attributes to the record and to find
STI subclass from the scope attributes.

But the effect of `scoping` is class global, it was caused undesired
behavior that pollute all class level querying methods in initialization
block and callbacks (`after_initialize`, `before_validation`,
`before_save`, etc), which are user provided code.

To avoid the leaking scope issue, restore the original current scope
before initialization block and callbacks are invoked.

Fixes rails#9894.
Fixes rails#17577.
Closes rails#31526.
@kamipo kamipo force-pushed the fix_leaking_scope_on_relation_create branch from 3912c93 to 2236053 Compare February 7, 2019 12:07
@kamipo kamipo merged commit b67d5c6 into rails:master Feb 7, 2019
@kamipo kamipo deleted the fix_leaking_scope_on_relation_create branch February 7, 2019 14:18
Copy link
Member

@rafaelfranca rafaelfranca left a comment

Choose a reason for hiding this comment

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

Would not this cause breaking changes?

@@ -67,6 +67,11 @@ def bind_attribute(name, value) # :nodoc:
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
def new(attributes = nil, &block)
current_scope = klass.current_scope(true)
Copy link
Member

Choose a reason for hiding this comment

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

It feels to me that this is being done at the wrong level. Relation should not know about scoping. Maybe this should be done in the scoping class?

cc @matthewd

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 agree that here is not best place to do this.
But Relation has scoping method, and already need to know the result of klass.current_scope(true) in some places (in scoping, spawn, etc), so I just do this here since here is only place to do this.

If we'd not like to do this here, an option is just extracting to method in the scoping class like the following:

diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 32f0609798..347d745d19 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -67,11 +67,7 @@ def bind_attribute(name, value) # :nodoc:
     #   user = users.new { |user| user.name = 'Oscar' }
     #   user.name # => Oscar
     def new(attributes = nil, &block)
-      current_scope = klass.current_scope(true)
-      block = -> record do
-        klass.current_scope = current_scope
-        yield record if block_given?
-      end
+      block = klass.current_scope_restoring_block(&block)
       scoping { klass.new(attributes, &block) }
     end
 
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 35e9dcbffc..1142a87d25 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -30,6 +30,14 @@ def current_scope(skip_inherited_scope = false)
       def current_scope=(scope)
         ScopeRegistry.set_value_for(:current_scope, self, scope)
       end
+
+      def current_scope_restoring_block(&block)
+        current_scope = self.current_scope(true)
+        -> *args do
+          self.current_scope = current_scope
+          yield(*args) if block_given?
+        end
+      end
     end
 
     def populate_with_current_scope_attributes # :nodoc:

Copy link
Member

Choose a reason for hiding this comment

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

That could be a nice refactoring

Copy link
Member Author

Choose a reason for hiding this comment

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

Applied 4cb1438.


attr_accessor :total_count
after_initialize do
self.total_count = Bird.count
Copy link
Member Author

Choose a reason for hiding this comment

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

Would not this cause breaking changes?

Before this PR #35186, the self.total_count = Bird.count in after_initialize and the Bird.find_by!(name: "canary") are affected by Bird.where(color: "green").scoping { } in Bird.where(color: "green").create, so parrot.total_count will be 0 and Bird.find_by!(name: "canary") will raise RecordNotFound.

canary = Bird.create!(color: "yellow", name: "canary")
parrot = Bird.where(color: "green").create do |bird|
  bird.name = "parrot"
  assert_equal canary, Bird.find_by!(name: "canary")
end
assert_equal 1, parrot.total_count

If anyone depends on the behavior, it is affected by this change.
But I think the leaking scope is just a bug, probably most people use a workaround Bird.unscoped.count and Bird.unscoped.find_by!(name: "canary") for a long time.

Copy link
Member

@rafaelfranca rafaelfranca Feb 8, 2019

Choose a reason for hiding this comment

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

👍

Copy link

Choose a reason for hiding this comment

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

@kamipo Thank you for the fix :-)

Copy link
Member

Choose a reason for hiding this comment

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

I'm still concerned about this breaking change. If people is relying on this behavior they may start to leak information silently like happened some years ago in the GitHub incident.

Is there any way we can detect people is relying on this behavior and show them a warning?

@tenderlove @matthewd @jeremy what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Please correct me if I'm wrong, but if the scope is leaking, then the scope in after_initialize etc would depend on the context in which the object is created. People depending on this behavior would have to have exactly one code path for initialization, otherwise they would have to do unscoped because the scope would be different each time.

Could we set the scope object to something that raises an exception if unscoped isn't called?

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 scoping is public API, people may have a usage like the case (1).
In the case (1), initialization block and callbacks are affected by the Bird.where(color: "yellow").scoping.
I made the case (2) working the same with the case (1) in this PR.

We could set the exception scope object for the case (2), it will make the case (2) working different back with the case (1) though...

canary = Bird.create!(color: "yellow", name: "canary")

# (1)
Bird.where(color: "yellow").scoping do
  parrot = Bird.create!(color: "green") do |bird|
    bird.name = "parrot"
    assert_equal canary, Bird.find_by!(name: "canary")
  end
  assert_equal 1, parrot.total_count
end

# (2)
Bird.where(color: "yellow").scoping do
  parrot = Bird.rewhere(color: "green").create! do |bird|
    bird.name = "parrot"
    assert_equal canary, Bird.find_by!(name: "canary")
  end
  assert_equal 1, parrot.total_count
end

kamipo added a commit to kamipo/rails that referenced this pull request Feb 15, 2019
…on_relation_create"

This reverts commit b67d5c6, reversing
changes made to 2e01836.

Reason: rails#35186 may cause that silently leaking information when people
upgrade the app.

We need deprecation first before making this.
kamipo added a commit to kamipo/rails that referenced this pull request Feb 15, 2019
…garded as leaked

This deprecates using class level querying methods if the receiver scope
regarded as leaked, since rails#32380 and rails#35186 may cause that silently
leaking information when people upgrade the app.

We need deprecation first before making those.
matthias-g added a commit to tim3z/kaheim that referenced this pull request May 25, 2019
In Subscription use unscoped because of rails/rails#35186
Matthijsy added a commit to csvalpha/amber-api that referenced this pull request Nov 25, 2019
github-actions bot pushed a commit to csvalpha/amber-api that referenced this pull request Dec 7, 2019
* Update gemfile and run rails app:upgrade

* Fix optional belongs to in mail alias

* Temp disable bullet

* Fix scope deprecation warnings (rails/rails#35186)

* Fix mail delivery job deprecation (rails/rails#34591)

* Switch to load 6.0
- Fix deprecation warnings about initalizers
- Fix model_name eager loading
- Fix autoimport
- Fix carrierwave

* Cleanup

* Rubocop

* Re-enable bullet

* Fix bullet

* Update lockfile

* Update activity spec and rubocop-performance version

* rubocop

* Typo

* Remove secret token since it is not used since rails 6

* Remove carrierwave uploader

* Sentry.rb

* Add additional test case for accept mail
kamipo added a commit that referenced this pull request May 7, 2020
rafaelfranca pushed a commit that referenced this pull request May 11, 2020
rafaelfranca pushed a commit that referenced this pull request Oct 30, 2020
alexdunae added a commit to culturecode/spatial_features that referenced this pull request Jun 3, 2021
alexdunae added a commit to culturecode/spatial_features that referenced this pull request Jun 4, 2021
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.

4 participants