flavour_saver 4.0.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +14 -0
  3. data/.standard.yml +1 -0
  4. data/CHANGELOG.md +57 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +48 -5
  7. data/README.md +153 -271
  8. data/flavour_saver.gemspec +17 -19
  9. data/lib/flavour_saver/error.rb +5 -0
  10. data/lib/flavour_saver/helpers.rb +19 -18
  11. data/lib/flavour_saver/lexer.rb +123 -50
  12. data/lib/flavour_saver/nodes.rb +107 -19
  13. data/lib/flavour_saver/parser.rb +339 -161
  14. data/lib/flavour_saver/partial.rb +4 -6
  15. data/lib/flavour_saver/rails_partial.rb +10 -4
  16. data/lib/flavour_saver/runtime.rb +76 -81
  17. data/lib/flavour_saver/template.rb +3 -5
  18. data/lib/flavour_saver/version.rb +1 -1
  19. data/lib/flavour_saver.rb +17 -17
  20. data/spec/acceptance/backtrack_spec.rb +9 -9
  21. data/spec/acceptance/comment_spec.rb +7 -7
  22. data/spec/acceptance/custom_block_helper_spec.rb +12 -12
  23. data/spec/acceptance/custom_helper_spec.rb +8 -8
  24. data/spec/acceptance/ensure_no_rce_spec.rb +64 -53
  25. data/spec/acceptance/handlebars_qunit_spec.rb +295 -297
  26. data/spec/acceptance/if_else_spec.rb +6 -6
  27. data/spec/acceptance/multi_level_with_spec.rb +10 -11
  28. data/spec/acceptance/nesting_depth_spec.rb +34 -0
  29. data/spec/acceptance/one_character_identifier_spec.rb +8 -8
  30. data/spec/acceptance/raw_block_spec.rb +7 -7
  31. data/spec/acceptance/sections_spec.rb +17 -14
  32. data/spec/acceptance/segment_literals_spec.rb +5 -5
  33. data/spec/acceptance/simple_expression_spec.rb +9 -9
  34. data/spec/acceptance/subexpression_spec.rb +18 -17
  35. data/spec/acceptance/unless_spec.rb +19 -6
  36. data/spec/lib/flavour_saver/lexer_spec.rb +103 -89
  37. data/spec/lib/flavour_saver/nodes_spec.rb +85 -0
  38. data/spec/lib/flavour_saver/parser_spec.rb +155 -95
  39. data/spec/lib/flavour_saver/runtime_spec.rb +94 -96
  40. data/spec/lib/flavour_saver/template_spec.rb +1 -2
  41. metadata +19 -48
@@ -1,11 +1,13 @@
1
- require 'tilt'
2
- require 'tmpdir'
3
- require 'fileutils'
1
+ # The payloads below set $fs_rce_canary so a spec can tell that one executed.
2
+ # standard:disable Style/GlobalVars
3
+ require "tilt"
4
+ require "tmpdir"
5
+ require "fileutils"
4
6
  # Object#try is mixed into Object from ActiveSupport's own module and forwards
5
7
  # to public_send, so it is part of the surface under test here. Required
6
8
  # explicitly rather than relying on another spec file having loaded it.
7
- require 'active_support/core_ext/object/try'
8
- require 'flavour_saver'
9
+ require "active_support/core_ext/object/try"
10
+ require "flavour_saver"
9
11
 
10
12
  # These specs previously drove Tilt.new(template_string), which treats its
11
13
  # argument as a *filename*. It raised "No template engine registered for
@@ -22,18 +24,18 @@ describe "Template-driven method dispatch" do
22
24
 
23
25
  describe "refuses methods inherited from Object" do
24
26
  {
25
- 'instance_eval' => '{{instance_eval "$fs_rce_canary = :pwned"}}',
26
- 'instance_exec' => '{{instance_exec "$fs_rce_canary = :pwned"}}',
27
- 'send' => '{{send "instance_eval" "$fs_rce_canary = :pwned"}}',
28
- '__send__' => '{{__send__ "instance_eval" "$fs_rce_canary = :pwned"}}',
29
- 'public_send' => '{{public_send "instance_eval" "$fs_rce_canary = :pwned"}}',
30
- 'method' => '{{method "instance_eval"}}',
31
- 'object path form' => '{{this.instance_eval "$fs_rce_canary = :pwned"}}',
32
- 'subexpression form' => '{{log (instance_eval "$fs_rce_canary = :pwned")}}',
27
+ "instance_eval" => '{{instance_eval "$fs_rce_canary = :pwned"}}',
28
+ "instance_exec" => '{{instance_exec "$fs_rce_canary = :pwned"}}',
29
+ "send" => '{{send "instance_eval" "$fs_rce_canary = :pwned"}}',
30
+ "__send__" => '{{__send__ "instance_eval" "$fs_rce_canary = :pwned"}}',
31
+ "public_send" => '{{public_send "instance_eval" "$fs_rce_canary = :pwned"}}',
32
+ "method" => '{{method "instance_eval"}}',
33
+ "object path form" => '{{this.instance_eval "$fs_rce_canary = :pwned"}}',
34
+ "subexpression form" => '{{log (instance_eval "$fs_rce_canary = :pwned")}}',
33
35
  # Decorator#method_missing is public and forwards to the context, and
34
36
  # method_missing is private on BasicObject so it never appeared in
35
37
  # Object.instance_methods.
36
- 'method_missing' => '{{method_missing "instance_eval" "$fs_rce_canary = :pwned"}}',
38
+ "method_missing" => '{{method_missing "instance_eval" "$fs_rce_canary = :pwned"}}'
37
39
  }.each do |description, template|
38
40
  it "refuses #{description}" do
39
41
  $fs_rce_canary = nil
@@ -46,7 +48,7 @@ describe "Template-driven method dispatch" do
46
48
  end
47
49
  end
48
50
 
49
- it 'refuses block expression form' do
51
+ it "refuses block expression form" do
50
52
  $fs_rce_canary = nil
51
53
 
52
54
  expect {
@@ -56,13 +58,13 @@ describe "Template-driven method dispatch" do
56
58
  expect($fs_rce_canary).to be_nil
57
59
  end
58
60
 
59
- it 'does not shell out via send' do
60
- expect { evaluate(%q({{send "system" "echo pwned"}})) }
61
+ it "does not shell out via send" do
62
+ expect { evaluate('{{send "system" "echo pwned"}}') }
61
63
  .to raise_error(FlavourSaver::ForbiddenMethodException)
62
64
  end
63
65
 
64
- it 'refuses regardless of the context object' do
65
- [Object.new, { 'a' => 'b' }, Struct.new(:name).new('x'), 'a string', []].each do |ctx|
66
+ it "refuses regardless of the context object" do
67
+ [Object.new, {"a" => "b"}, Struct.new(:name).new("x"), "a string", []].each do |ctx|
66
68
  $fs_rce_canary = nil
67
69
 
68
70
  expect { evaluate('{{instance_eval "$fs_rce_canary = :pwned"}}', ctx) }
@@ -76,7 +78,7 @@ describe "Template-driven method dispatch" do
76
78
  # raises TypeError rather than NameError. It must still resolve to a refusal
77
79
  # (a primitive cannot carry a singleton method) rather than leaking an
78
80
  # unrescued TypeError past the UnknownHelperException contract.
79
- it 'refuses on primitive contexts without leaking TypeError' do
81
+ it "refuses on primitive contexts without leaking TypeError" do
80
82
  [5, :sym, 1.5].each do |ctx|
81
83
  $fs_rce_canary = nil
82
84
 
@@ -90,14 +92,14 @@ describe "Template-driven method dispatch" do
90
92
  # The private forbidden_method? predicate must be correct on its own, not
91
93
  # only behind the decorator wrapping in evaluate_call, since a primitive
92
94
  # reaching it undecorated is the exact case that raised TypeError.
93
- it 'the predicate refuses inherited methods on an undecorated primitive' do
94
- runtime = FlavourSaver::Runtime.new(FlavourSaver.parse(FlavourSaver.lex('')), context)
95
- expect(runtime.send(:forbidden_method?, 5, 'instance_eval')).to be true
95
+ it "the predicate refuses inherited methods on an undecorated primitive" do
96
+ runtime = FlavourSaver::Runtime.new(FlavourSaver.parse(FlavourSaver.lex("")), context)
97
+ expect(runtime.send(:forbidden_method?, 5, "instance_eval")).to be true
96
98
  # ...but leaves a primitive's own domain method dispatchable.
97
- expect(runtime.send(:forbidden_method?, 5, 'bit_length')).to be false
99
+ expect(runtime.send(:forbidden_method?, 5, "bit_length")).to be false
98
100
  end
99
101
 
100
- it 'is catchable as UnknownHelperException, for existing rescue clauses' do
102
+ it "is catchable as UnknownHelperException, for existing rescue clauses" do
101
103
  expect { evaluate('{{instance_eval "1"}}') }
102
104
  .to raise_error(FlavourSaver::UnknownHelperException)
103
105
  end
@@ -106,9 +108,9 @@ describe "Template-driven method dispatch" do
106
108
  # helper list when that list is non-empty. A guard that consulted the global
107
109
  # registry would exempt a name that was never mixed in, and dispatch would
108
110
  # reach Object's implementation of it.
109
- it 'refuses a registered helper name that was scoped out of this runtime' do
111
+ it "refuses a registered helper name that was scoped out of this runtime" do
110
112
  $fs_rce_canary = nil
111
- FlavourSaver::Helpers.register_helper(:send) { 'helper!' }
113
+ FlavourSaver::Helpers.register_helper(:send) { "helper!" }
112
114
 
113
115
  ast = FlavourSaver.parse(FlavourSaver.lex('{{send "instance_eval" "$fs_rce_canary = :pwned"}}'))
114
116
 
@@ -131,7 +133,7 @@ describe "Template-driven method dispatch" do
131
133
  canary = File.join(Dir.tmpdir, "fs_rce_#{Process.pid}_#{rand(1 << 32)}")
132
134
  begin
133
135
  evaluate(format(template, canary), ctx)
134
- rescue StandardError
136
+ rescue
135
137
  # An exception is an acceptable outcome; execution is not.
136
138
  end
137
139
  expect(File.exist?(canary)).to be false
@@ -147,12 +149,16 @@ describe "Template-driven method dispatch" do
147
149
 
148
150
  # The OSVDB-110796 payload: a proxy or delegator answering true to
149
151
  # everything used to make every private Kernel method reachable.
150
- it 'does not execute against a context with a permissive respond_to?' do
151
- permissive = Class.new { def respond_to?(name, priv = false); true; end }.new
152
+ it "does not execute against a context with a permissive respond_to?" do
153
+ permissive = Class.new {
154
+ def respond_to?(name, priv = false)
155
+ true
156
+ end
157
+ }.new
152
158
  expect_no_execution('{{system "touch %s"}}', permissive)
153
159
  end
154
160
 
155
- it 'does not execute via the send trampoline' do
161
+ it "does not execute via the send trampoline" do
156
162
  expect_no_execution('{{send "system" "touch %s"}}', context)
157
163
  end
158
164
  end
@@ -160,7 +166,7 @@ describe "Template-driven method dispatch" do
160
166
  # Object#try forwards to public_send, so a guard keyed on the method's owner
161
167
  # being Object/Kernel/BasicObject would miss it: ActiveSupport mixes try into
162
168
  # Object via its own module.
163
- it 'refuses methods mixed into Object by a framework' do
169
+ it "refuses methods mixed into Object by a framework" do
164
170
  $fs_rce_canary = nil
165
171
 
166
172
  expect { evaluate('{{try "instance_eval" "$fs_rce_canary = :pwned"}}') }
@@ -177,8 +183,8 @@ describe "Template-driven method dispatch" do
177
183
  # No predicate names here: the lexer's IDENT rule is /([A-Za-z_]\w*)/, so a
178
184
  # trailing ? cannot form part of a template identifier.
179
185
  %w[instance_eval instance_exec send __send__ public_send method
180
- define_singleton_method singleton_class class extend freeze
181
- method_missing tap then itself].each do |name|
186
+ define_singleton_method singleton_class class extend freeze
187
+ method_missing tap then itself].each do |name|
182
188
  it "refuses #{name}" do
183
189
  expect { evaluate("{{#{name}}}") }
184
190
  .to raise_error(FlavourSaver::ForbiddenMethodException)
@@ -193,9 +199,9 @@ describe "Template-driven method dispatch" do
193
199
  # suite short-circuited. Point it at a real .hbs fixture, as the other
194
200
  # fixture specs do.
195
201
  describe "refuses payloads in templates loaded from disk" do
196
- let(:fixture) { File.expand_path('../../fixtures/rce.hbs', __FILE__) }
202
+ let(:fixture) { File.expand_path("../../fixtures/rce.hbs", __FILE__) }
197
203
 
198
- it 'refuses when rendered through Tilt' do
204
+ it "refuses when rendered through Tilt" do
199
205
  $fs_rce_canary = nil
200
206
 
201
207
  expect { Tilt.new(fixture).render(context) }
@@ -204,41 +210,45 @@ describe "Template-driven method dispatch" do
204
210
  expect($fs_rce_canary).to be_nil
205
211
  end
206
212
 
207
- it 'has a fixture that really does contain a payload' do
208
- expect(File.read(fixture)).to include 'instance_eval'
213
+ it "has a fixture that really does contain a payload" do
214
+ expect(File.read(fixture)).to include "instance_eval"
209
215
  end
210
216
  end
211
217
 
212
218
  # The fix must not be so broad that it breaks ordinary templates.
213
219
  describe "still allows legitimate dispatch" do
214
- it 'calls a method the context actually defines' do
215
- klass = Class.new { def greeting; 'hello'; end }
216
- expect(evaluate('{{greeting}}', klass.new)).to eq 'hello'
220
+ it "calls a method the context actually defines" do
221
+ klass = Class.new {
222
+ def greeting
223
+ "hello"
224
+ end
225
+ }
226
+ expect(evaluate("{{greeting}}", klass.new)).to eq "hello"
217
227
  end
218
228
 
219
- it 'reads a data field named after an Object method via segment literals' do
220
- expect(evaluate('{{[class]}}', { 'class' => 'btn-primary' })).to eq 'btn-primary'
221
- expect(evaluate('{{[hash]}}', { 'hash' => 'abc123' })).to eq 'abc123'
229
+ it "reads a data field named after an Object method via segment literals" do
230
+ expect(evaluate("{{[class]}}", {"class" => "btn-primary"})).to eq "btn-primary"
231
+ expect(evaluate("{{[hash]}}", {"hash" => "abc123"})).to eq "abc123"
222
232
  end
223
233
 
224
- it 'allows a deliberately registered helper to shadow an Object method' do
225
- FlavourSaver::Helpers.register_helper(:hash) { 'abc123' }
226
- expect(evaluate('{{hash}}')).to eq 'abc123'
234
+ it "allows a deliberately registered helper to shadow an Object method" do
235
+ FlavourSaver::Helpers.register_helper(:hash) { "abc123" }
236
+ expect(evaluate("{{hash}}")).to eq "abc123"
227
237
  ensure
228
238
  FlavourSaver::Helpers.deregister_helper(:hash)
229
239
  end
230
240
 
231
- it 'allows a local to shadow an Object method' do
241
+ it "allows a local to shadow an Object method" do
232
242
  runtime = FlavourSaver::Runtime.new(
233
- FlavourSaver.parse(FlavourSaver.lex('{{hash}}')),
243
+ FlavourSaver.parse(FlavourSaver.lex("{{hash}}")),
234
244
  context,
235
- { 'hash' => proc { 'abc123' } }
245
+ {"hash" => proc { "abc123" }}
236
246
  )
237
- expect(runtime.to_s).to eq 'abc123'
247
+ expect(runtime.to_s).to eq "abc123"
238
248
  end
239
249
 
240
- it 'still renders nothing for an unknown, non-forbidden method' do
241
- expect(evaluate('{{no_such_method}}')).to eq ''
250
+ it "still renders nothing for an unknown, non-forbidden method" do
251
+ expect(evaluate("{{no_such_method}}")).to eq ""
242
252
  end
243
253
 
244
254
  # Roughly sixty lexable names collide with a private Kernel method. They are
@@ -252,3 +262,4 @@ describe "Template-driven method dispatch" do
252
262
  end
253
263
  end
254
264
  end
265
+ # standard:enable Style/GlobalVars