Skip to content

Commit 538582d

Browse files
authored
Merge pull request from GHSA-f78j-4w3g-4q65
* Validate Reflex `method_name` to only allow safe reflex method-calls * Only allow public instance methods * Allow reflex methods from concerns to be called
1 parent 078db30 commit 538582d

File tree

3 files changed

+148
-2
lines changed

3 files changed

+148
-2
lines changed

lib/stimulus_reflex/reflex_factory.rb

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,57 @@
33
class StimulusReflex::ReflexFactory
44
attr_reader :channel, :data
55

6-
delegate :reflex_name, to: :data
6+
delegate :reflex_name, :method_name, to: :data
77

88
def initialize(channel, data)
99
@channel = channel
1010
@data = StimulusReflex::ReflexData.new(data)
1111
end
1212

1313
def call
14+
verify_method_name!
1415
reflex_class.new(channel, reflex_data: data)
1516
end
1617

1718
private
1819

20+
def verify_method_name!
21+
return if default_reflex?
22+
23+
argument_error = ArgumentError.new("Reflex method '#{method_name}' is not defined on class '#{reflex_name}' or on any of its ancestors")
24+
25+
if reflex_method.nil?
26+
raise argument_error
27+
end
28+
29+
if !safe_ancestors.include?(reflex_method.owner)
30+
raise argument_error
31+
end
32+
end
33+
1934
def reflex_class
20-
reflex_name.constantize.tap do |klass|
35+
@reflex_class ||= reflex_name.constantize.tap do |klass|
2136
unless klass.ancestors.include?(StimulusReflex::Reflex)
2237
raise ArgumentError.new("#{reflex_name} is not a StimulusReflex::Reflex")
2338
end
2439
end
2540
end
41+
42+
def reflex_method
43+
if reflex_class.public_instance_methods.include?(method_name.to_sym)
44+
reflex_class.public_instance_method(method_name)
45+
end
46+
end
47+
48+
def default_reflex?
49+
method_name == "default_reflex" && reflex_method.owner == ::StimulusReflex::Reflex
50+
end
51+
52+
def safe_ancestors
53+
# We want to include every class and module up to the `StimulusReflex::Reflex` class,
54+
# but not the StimulusReflex::Reflex itself
55+
reflex_class_index = reflex_class.ancestors.index(StimulusReflex::Reflex) - 1
56+
57+
reflex_class.ancestors.to(reflex_class_index)
58+
end
2659
end

test/reflex_factory_test.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "test_helper"
4+
5+
class StimulusReflex::ReflexFactoryTest < ActionCable::Channel::TestCase
6+
tests StimulusReflex::Channel
7+
8+
test "reflex class needs to be an ancestor of StimulusReflex::Reflex" do
9+
exception = assert_raises(NameError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "Object#inspect"}).call }
10+
assert_equal "uninitialized constant ObjectReflex", exception.message
11+
12+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "NoReflex#no_reflex"}).call }
13+
assert_equal "NoReflex is not a StimulusReflex::Reflex", exception.message
14+
15+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "No#no_reflex"}).call }
16+
assert_equal "NoReflex is not a StimulusReflex::Reflex", exception.message
17+
end
18+
19+
test "doesn't raise if owner of method is ancestor of reflex class and descendant of StimulusReflex::Reflex" do
20+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "ApplicationReflex#default_reflex"}).call }
21+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "ApplicationReflex#application_reflex"}).call }
22+
23+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "PostReflex#default_reflex"}).call }
24+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "PostReflex#application_reflex"}).call }
25+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "PostReflex#post_reflex"}).call }
26+
27+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "CounterReflex#default_reflex"}).call }
28+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "CounterReflex#application_reflex"}).call }
29+
assert_nothing_raised { StimulusReflex::ReflexFactory.new(subscribe, {version: StimulusReflex::VERSION, target: "CounterReflex#increment"}).call }
30+
end
31+
32+
test "raises if method is not owned by a descendant of StimulusReflex::Reflex" do
33+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "ApplicationReflex#itself"}).call }
34+
assert_equal "Reflex method 'itself' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message
35+
36+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "ApplicationReflex#itself"}).call }
37+
assert_equal "Reflex method 'itself' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message
38+
39+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "PostReflex#itself"}).call }
40+
assert_equal "Reflex method 'itself' is not defined on class 'PostReflex' or on any of its ancestors", exception.message
41+
42+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "PostReflex#binding"}).call }
43+
assert_equal "Reflex method 'binding' is not defined on class 'PostReflex' or on any of its ancestors", exception.message
44+
45+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "PostReflex#byebug"}).call }
46+
assert_equal "Reflex method 'byebug' is not defined on class 'PostReflex' or on any of its ancestors", exception.message
47+
48+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "PostReflex#debug"}).call }
49+
assert_equal "Reflex method 'debug' is not defined on class 'PostReflex' or on any of its ancestors", exception.message
50+
51+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "ApplicationReflex#post_reflex"}).call }
52+
assert_equal "Reflex method 'post_reflex' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message
53+
end
54+
55+
test "raises if method is a private method" do
56+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "ApplicationReflex#private_application_reflex"}).call }
57+
assert_equal "Reflex method 'private_application_reflex' is not defined on class 'ApplicationReflex' or on any of its ancestors", exception.message
58+
59+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "PostReflex#private_application_reflex"}).call }
60+
assert_equal "Reflex method 'private_application_reflex' is not defined on class 'PostReflex' or on any of its ancestors", exception.message
61+
62+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "PostReflex#private_post_reflex"}).call }
63+
assert_equal "Reflex method 'private_post_reflex' is not defined on class 'PostReflex' or on any of its ancestors", exception.message
64+
65+
exception = assert_raises(ArgumentError) { StimulusReflex::ReflexFactory.new(subscribe, {target: "CounterReflex#private_post_reflex"}).call }
66+
assert_equal "Reflex method 'private_post_reflex' is not defined on class 'CounterReflex' or on any of its ancestors", exception.message
67+
end
68+
69+
test "safe_ancestors" do
70+
reflex_factory = StimulusReflex::ReflexFactory.new(subscribe, {target: "ApplicationReflex#default_reflex"})
71+
assert_equal [ApplicationReflex, StimulusReflex::CableReadiness], reflex_factory.send(:safe_ancestors)
72+
73+
reflex_factory = StimulusReflex::ReflexFactory.new(subscribe, {target: "PostReflex#default_reflex"})
74+
assert_equal [PostReflex, ApplicationReflex, StimulusReflex::CableReadiness], reflex_factory.send(:safe_ancestors)
75+
76+
reflex_factory = StimulusReflex::ReflexFactory.new(subscribe, {target: "CounterReflex#increment"})
77+
assert_equal [CounterReflex, CounterConcern, ApplicationReflex, StimulusReflex::CableReadiness], reflex_factory.send(:safe_ancestors)
78+
end
79+
end

test/test_helper.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,40 @@ def to_gid_param
5050
end
5151
end
5252

53+
class ApplicationReflex < StimulusReflex::Reflex
54+
def application_reflex
55+
end
56+
57+
private
58+
59+
def private_application_reflex
60+
end
61+
end
62+
63+
class PostReflex < ApplicationReflex
64+
def post_reflex
65+
end
66+
67+
private
68+
69+
def private_post_reflex
70+
end
71+
end
72+
73+
class NoReflex
74+
def no_reflex
75+
end
76+
end
77+
78+
module CounterConcern
79+
def increment
80+
end
81+
end
82+
83+
class CounterReflex < ApplicationReflex
84+
include CounterConcern
85+
end
86+
5387
module ActionCable
5488
module Channel
5589
class ConnectionStub

0 commit comments

Comments
 (0)