From c698474abc9ab004812f78b8adee4d5c16e9f7b1 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Wed, 6 May 2026 09:49:30 +0200 Subject: [PATCH 1/5] Prevent `SelfDrop` context mutation across render boundaries --- lib/liquid/self_drop.rb | 8 ++-- test/integration/self_drop_test.rb | 71 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 test/integration/self_drop_test.rb diff --git a/lib/liquid/self_drop.rb b/lib/liquid/self_drop.rb index 357814653..fa3fa27a2 100644 --- a/lib/liquid/self_drop.rb +++ b/lib/liquid/self_drop.rb @@ -16,19 +16,19 @@ module Liquid # then the local value takes precedence over the `self` object. # @liquid_access global class SelfDrop < Drop - def initialize(context) + def initialize(self_context) super() - @context = context + @self_context = self_context end def [](key) - @context.find_variable(key) + @self_context.find_variable(key) rescue UndefinedVariable nil end def key?(key) - @context.variable_defined?(key) + @self_context.variable_defined?(key) end def to_liquid diff --git a/test/integration/self_drop_test.rb b/test/integration/self_drop_test.rb new file mode 100644 index 000000000..39775546d --- /dev/null +++ b/test/integration/self_drop_test.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'test_helper' + +class SelfDropTest < Minitest::Test + include Liquid + + def test_self_drop_passed_as_render_param_preserves_original_scope + source = <<~LIQUID + {%- assign var = 42 -%} + {%- assign s = self -%} + {%- render "snippet1", other_self: s -%} + LIQUID + + partials = { + 'snippet1' => <<~LIQUID, + {%- assign var = 43 -%} + {{- other_self.var }}|{{ self.var -}} + LIQUID + } + + assert_template_result('42|43', source, partials: partials) + end + + def test_self_drop_in_render_without_passing_resolves_inner_scope + source = <<~LIQUID + {%- assign var = 42 -%} + {%- render "snippet1" -%} + LIQUID + + partials = { + 'snippet1' => <<~LIQUID, + {%- assign var = 99 -%} + {{- self.var -}} + LIQUID + } + + assert_template_result('99', source, partials: partials) + end + + def test_self_drop_passed_to_nested_renders_preserves_each_level + source = <<~LIQUID + {%- assign a = 1 -%} + {%- assign s1 = self -%} + {%- render "snippet1", outer: s1 -%} + LIQUID + + partials = { + 'snippet1' => <<~LIQUID, + {%- assign a = 2 -%} + {%- assign s2 = self -%} + {%- render "snippet2", outer: outer, middle: s2 -%} + LIQUID + 'snippet2' => <<~LIQUID, + {%- assign a = 3 -%} + {{- outer.a }}|{{ middle.a }}|{{ self.a -}} + LIQUID + } + + assert_template_result('1|2|3', source, partials: partials) + end + + def test_self_drop_reflects_variables_assigned_after_creation + source = <<~LIQUID + {%- assign s = self -%} + {%- assign x = 42 %}{{ s.x -}} + LIQUID + + assert_template_result('42', source) + end +end From fd49bbafb5f715947803c898d9485f3e89b4da85 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Wed, 6 May 2026 16:06:38 +0200 Subject: [PATCH 2/5] Renamed: test/integration/self_drop_test.rb -> test/integration/self_drop_context_test.rb --- .../{self_drop_test.rb => self_drop_context_test.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/integration/{self_drop_test.rb => self_drop_context_test.rb} (97%) diff --git a/test/integration/self_drop_test.rb b/test/integration/self_drop_context_test.rb similarity index 97% rename from test/integration/self_drop_test.rb rename to test/integration/self_drop_context_test.rb index 39775546d..86c21c0a5 100644 --- a/test/integration/self_drop_test.rb +++ b/test/integration/self_drop_context_test.rb @@ -2,7 +2,7 @@ require 'test_helper' -class SelfDropTest < Minitest::Test +class SelfDropContextTest < Minitest::Test include Liquid def test_self_drop_passed_as_render_param_preserves_original_scope From bc7f1caa35368f9628b1f1f3d173262e8b50b526 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Fri, 8 May 2026 09:51:31 +0200 Subject: [PATCH 3/5] Remove support for contextualization --- lib/liquid/self_drop.rb | 2 ++ test/integration/self_drop_context_test.rb | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/liquid/self_drop.rb b/lib/liquid/self_drop.rb index fa3fa27a2..2f54fd386 100644 --- a/lib/liquid/self_drop.rb +++ b/lib/liquid/self_drop.rb @@ -31,6 +31,8 @@ def key?(key) @self_context.variable_defined?(key) end + def context=(_); end + def to_liquid self end diff --git a/test/integration/self_drop_context_test.rb b/test/integration/self_drop_context_test.rb index 86c21c0a5..9a1c86cff 100644 --- a/test/integration/self_drop_context_test.rb +++ b/test/integration/self_drop_context_test.rb @@ -68,4 +68,24 @@ def test_self_drop_reflects_variables_assigned_after_creation assert_template_result('42', source) end + + def test_self_drop_context_writer_is_a_noop + context = Context.new + drop = SelfDrop.new(context) + assert(drop.respond_to?(:context=)) + drop.context = Context.new + assert_nil(drop.instance_variable_get(:@context)) + end + + def test_self_drop_with_strict_variables_does_not_raise_for_defined_var + t = Template.parse('{{ self.x }}') + result = t.render({ 'x' => 42 }, strict_variables: true) + assert_equal('42', result) + end + + def test_self_drop_with_strict_variables_returns_nil_for_undefined_var + t = Template.parse('{{ self.x }}') + result = t.render({}, strict_variables: true) + assert_equal('', result) + end end From 8d8105a4529f1e1fbb531e05af275c168218297b Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Wed, 13 May 2026 09:11:10 +0200 Subject: [PATCH 4/5] Update lib/liquid/self_drop.rb Co-authored-by: Ian Ker-Seymer --- lib/liquid/self_drop.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/liquid/self_drop.rb b/lib/liquid/self_drop.rb index 2f54fd386..045653a5c 100644 --- a/lib/liquid/self_drop.rb +++ b/lib/liquid/self_drop.rb @@ -31,7 +31,7 @@ def key?(key) @self_context.variable_defined?(key) end - def context=(_); end + undef context def to_liquid self From 3f5a7faeb246d5d0b242292976893dedc421c58c Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Wed, 13 May 2026 10:36:44 +0200 Subject: [PATCH 5/5] Adopt 'undef context=' --- lib/liquid/self_drop.rb | 4 ++-- lib/liquid/template.rb | 6 ++++-- test/integration/self_drop_context_test.rb | 15 +++++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/liquid/self_drop.rb b/lib/liquid/self_drop.rb index 045653a5c..4bfff0e45 100644 --- a/lib/liquid/self_drop.rb +++ b/lib/liquid/self_drop.rb @@ -31,10 +31,10 @@ def key?(key) @self_context.variable_defined?(key) end - undef context - def to_liquid self end + + undef context= end end diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index b007765ce..70ff00816 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -151,8 +151,10 @@ def render(*args) c when Liquid::Drop - drop = args.shift - drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment) + drop = args.shift + c = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment) + drop.context = c if drop.respond_to?(:context=) + c when Hash Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment) when nil diff --git a/test/integration/self_drop_context_test.rb b/test/integration/self_drop_context_test.rb index 9a1c86cff..338719e81 100644 --- a/test/integration/self_drop_context_test.rb +++ b/test/integration/self_drop_context_test.rb @@ -69,12 +69,12 @@ def test_self_drop_reflects_variables_assigned_after_creation assert_template_result('42', source) end - def test_self_drop_context_writer_is_a_noop + def test_self_drop_context_setter_is_undefined context = Context.new drop = SelfDrop.new(context) - assert(drop.respond_to?(:context=)) - drop.context = Context.new - assert_nil(drop.instance_variable_get(:@context)) + refute(drop.respond_to?(:context=)) + + assert_template_result('42', '{{ self.x }}', { 'x' => 42 }) end def test_self_drop_with_strict_variables_does_not_raise_for_defined_var @@ -88,4 +88,11 @@ def test_self_drop_with_strict_variables_returns_nil_for_undefined_var result = t.render({}, strict_variables: true) assert_equal('', result) end + + def test_self_drop_can_be_passed_as_bare_drop_to_render + t = Template.parse('{{ self.x }}') + drop = SelfDrop.new(Context.new({ 'x' => 42 })) + result = t.render(drop) + assert_equal('42', result) + end end