flavour_saver 4.0.0 → 4.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 479bb662042e1b7168a3a7db7b34094fc0e66c9a9da64240d4c1117e61345409
4
- data.tar.gz: 98d44dd0115bd00a2bf122f70602cab8854b7770f7513703ce0af606ac9c8bf9
3
+ metadata.gz: 453d625c7c8aad7ed0a3a06d21d482e92212e252e3a3e0624c825561cb117193
4
+ data.tar.gz: 2bae35d9e7f9c4330d93d4dbfdf72c2fb4d0ce09f660f9f80d78ff8095b249b0
5
5
  SHA512:
6
- metadata.gz: efeeb00e64f30bf3433f801b849cbfcfea494fd202a79823d680f7b38a6191d06c8b7fe5e2e6c55511bfb003ef9b71defd2b4ca00beb4a92d2d7eb5112b96122
7
- data.tar.gz: cf8d25967ac70181240e964cfab1018a371263f17db5ca2f251af87070e2db9dd16b1b68d4ff44b6f49fdc9c94fcc19e20e971f558e2c6fa86178aa740a522a3
6
+ metadata.gz: cab9b8e280389ec12e1dbb227698bb52e9ca30859fdb15103db7b5c35f7263e3d8355e05c94d364cda7d7278c7850155ac66ac9ef2027e617bde6ecf0e7387e6
7
+ data.tar.gz: c0d6c23e01a6850863092c1829310589e1fd6503f8337be215ad308d3487464cecf74330b4a42c29c937576a70b1b3e1f3a2f514adadfa3679c873006e417749
data/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 4.0.1
11
+
12
+ ### Security
13
+
14
+ * Fixed arbitrary code execution via template method dispatch
15
+ ([GHSA-98g2-gr9f-f85r](https://github.com/FlavourSaver/FlavourSaver/security/advisories/GHSA-98g2-gr9f-f85r)).
16
+ A bare Handlebars identifier was dispatched to the rendering context with
17
+ `public_send`, and Ruby's metaprogramming methods are public on every object, so a
18
+ template containing `{{instance_eval "..."}}` executed arbitrary Ruby in the host
19
+ process. Anyone able to author or edit a template could run code as the application.
20
+
21
+ **All releases up to and including 4.0.0 are affected.** The fix for OSVDB #110796
22
+ guarded dispatch with `respond_to?`. That never blocked public `Object` methods, so
23
+ `{{instance_eval "..."}}` was reachable before and after it, and it could be sidestepped
24
+ entirely with `{{send "system" "ls"}}`. It stopped `{{system "ls"}}` only for context
25
+ objects that report their methods honestly — against a proxy or delegator that
26
+ overreports `respond_to?`, that payload kept working in every release. This release is
27
+ what closes it.
28
+
29
+ Reported by Arpit Jain ([@arpitjain099](https://github.com/arpitjain099)).
30
+
31
+ ### Changed
32
+
33
+ * Templates may now only dispatch to methods the application deliberately made
34
+ available: registered helpers, locals, FlavourSaver's own block helpers, and the
35
+ context object's own API. Anything else — Ruby's inherited object surface, methods
36
+ a framework has mixed into `Object`, and FlavourSaver's internal plumbing — raises
37
+ `FlavourSaver::ForbiddenMethodException`, a subclass of `UnknownHelperException`, so
38
+ existing rescue clauses continue to catch it.
39
+
40
+ Templates using `{{class}}`, `{{hash}}`, `{{method}}`, `{{display}}` and similar will
41
+ now raise instead of rendering. Such templates could not have been working correctly:
42
+ `Helpers::Decorator` inherits those methods, so its own implementation already shadowed
43
+ any same-named method on the context object — `{{hash}}` returned an object digest and
44
+ `{{display}}` printed the decorator to stdout rather than returning your data.
45
+
46
+ Context methods sharing a name with a *private* `Kernel` method — `format`, `open`,
47
+ `select`, `print`, `test`, `load` and around sixty others — are unaffected and continue
48
+ to dispatch as before.
49
+
50
+ To read a data field named after an `Object` method, use the segment literal syntax
51
+ (`{{[hash]}}`), which resolves through `Decorator#[]` and so needs a hash-like context,
52
+ or register an explicit helper.
53
+
10
54
  ## 4.0.0
11
55
 
12
56
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- flavour_saver (4.0.0)
4
+ flavour_saver (4.0.1)
5
5
  rltk (< 3.0)
6
6
  tilt (~> 2.6)
7
7
 
@@ -92,7 +92,21 @@ module FlavourSaver
92
92
  # I would rather have it raise a NameError, but Moustache
93
93
  # compatibility requires that missing helpers return
94
94
  # nothing. A good place for bugs to hide.
95
- @source.send(name, *args, &b) if @source.respond_to? name
95
+ #
96
+ # SECURITY: public_send here is load-bearing, not stylistic. Do not
97
+ # change it back to send.
98
+ #
99
+ # Private Kernel methods -- system, eval, exec, fork -- are deliberately
100
+ # not refused by Runtime#forbidden_method?, because refusing them would
101
+ # also refuse the ~60 context methods that share a name with one. They
102
+ # are safe because they cannot be reached: public_send skips them, so
103
+ # they arrive here, and this call skips them again.
104
+ #
105
+ # That makes this the only thing standing between a context that
106
+ # overreports respond_to? -- a proxy or delegator answering true to
107
+ # everything -- and arbitrary command execution. Reverting it to send
108
+ # reopens {{system "..."}} on such a context; verified, not theorised.
109
+ @source.public_send(name, *args, &b) if @source.respond_to? name
96
110
  end
97
111
  end
98
112
 
@@ -6,6 +6,7 @@ module FlavourSaver
6
6
  InappropriateUseOfElseException = Class.new(StandardError)
7
7
  UndefinedPrivateVariableException = Class.new(StandardError)
8
8
  UnknownHelperException = Class.new(RuntimeError)
9
+ ForbiddenMethodException = Class.new(UnknownHelperException)
9
10
  class Runtime
10
11
 
11
12
  attr_accessor :context, :parent, :ast, :privates
@@ -128,6 +129,9 @@ module FlavourSaver
128
129
  when LocalVarNode
129
130
  result = private_variable_get(call.name)
130
131
  else
132
+ if forbidden_method? context, call.name
133
+ raise ForbiddenMethodException, "Refusing to call #{call.name.inspect} from a template: it isn't a helper, a local, or part of the template context's own API. Register a helper if you meant to expose it, or use #{"{{[#{call.name}]}}"} to read a data field of that name."
134
+ end
131
135
  if call.parent.is_a? BlockExpressionNode and !context.respond_to? call.name
132
136
  raise UnknownHelperException, "Template context doesn't respond to method #{call.name.inspect}."
133
137
  end
@@ -239,6 +243,56 @@ module FlavourSaver
239
243
 
240
244
  private
241
245
 
246
+ # True unless +name+ resolves to something the application deliberately
247
+ # made available to templates.
248
+ #
249
+ # This is an allowlist of *owners* rather than a denylist of names, because
250
+ # a name denylist cannot be made complete. Object's inherited API is the
251
+ # obvious hazard (instance_eval, send, instance_exec), but it is not the
252
+ # whole of it: ActiveSupport mixes Object#try in from its own module and it
253
+ # forwards to public_send, and Decorator#method_missing is public and
254
+ # forwards to the context. Neither appears in Object.instance_methods.
255
+ #
256
+ # Only two things are legitimately dispatchable on the decorator itself:
257
+ # helpers and locals, which live on a module extended onto this runtime's
258
+ # decorator and so are absent from Decorator.ancestors; and FlavourSaver's
259
+ # own helpers, which Defaults owns. Everything else defined on the
260
+ # decorator is either Object's ambient surface or the decorator's internal
261
+ # plumbing, and neither is template surface.
262
+ #
263
+ # A name the decorator doesn't define at all raises NameError here, which
264
+ # means it will be delegated to the context object by method_missing --
265
+ # ordinary template dispatch, and allowed.
266
+ #
267
+ # Only *publicly* reachable names are refused. #method resolves private
268
+ # methods too, and refusing those as well would break every context method
269
+ # sharing a name with a private Kernel method -- format, open, select,
270
+ # print, test, load and some sixty others, all of which dispatch fine
271
+ # today. They aren't reachable through public_send in any case: they fall
272
+ # through to method_missing, which is guarded by its own use of
273
+ # public_send.
274
+ def forbidden_method?(context, name)
275
+ owner = begin
276
+ context.method(name).owner
277
+ rescue NameError
278
+ return false
279
+ end
280
+
281
+ return false if owner.equal? Helpers::Defaults
282
+ return false unless Helpers::Decorator.ancestors.include? owner
283
+
284
+ begin
285
+ context.singleton_class.public_method_defined? name
286
+ rescue TypeError
287
+ # Integer, Symbol and Float have no singleton class. A primitive can't
288
+ # carry a singleton method, so a name that resolved on one is
289
+ # necessarily inherited -- refuse it. Reached only if a primitive is
290
+ # ever passed here undecorated; the decorator wrapping in evaluate_call
291
+ # normally prevents that, but the predicate must be correct on its own.
292
+ true
293
+ end
294
+ end
295
+
242
296
  def escape(output)
243
297
  if output.respond_to?(:html_safe) && output.html_safe?
244
298
  # If the string is already marked as html_safe then don't
@@ -1,3 +1,3 @@
1
1
  module FlavourSaver
2
- VERSION = "4.0.0"
2
+ VERSION = "4.0.1"
3
3
  end
@@ -1,26 +1,254 @@
1
1
  require 'tilt'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ # Object#try is mixed into Object from ActiveSupport's own module and forwards
5
+ # to public_send, so it is part of the surface under test here. Required
6
+ # explicitly rather than relying on another spec file having loaded it.
7
+ require 'active_support/core_ext/object/try'
2
8
  require 'flavour_saver'
3
9
 
4
- describe "Can't call methods that the context doesn't respond to" do
5
- subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
6
- let(:template) { '{{system "ls"}}' }
7
- let(:context) { double(:context) }
10
+ # These specs previously drove Tilt.new(template_string), which treats its
11
+ # argument as a *filename*. It raised "No template engine registered for
12
+ # {{system "ls"}}" before FlavourSaver was ever invoked, so both examples
13
+ # passed without executing a single line of the runtime. Drive
14
+ # FlavourSaver.evaluate directly so the dispatch path is actually exercised.
15
+ describe "Template-driven method dispatch" do
16
+ # A plain object, to show the context needs nothing special to be exploitable.
17
+ let(:context) { Object.new }
8
18
 
9
- it 'renders correctly' do
10
- expect(Kernel).not_to receive(:system)
11
- expect { subject }.to raise_error(RuntimeError)
19
+ def evaluate(template, ctx = context)
20
+ FlavourSaver.evaluate(template, ctx)
12
21
  end
13
- end
14
22
 
15
- describe "Can't eval arbitrary Ruby code" do
16
- subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
17
- let(:template) { '{{eval "puts 1 + 1"}}' }
18
- let(:context) { double(:context) }
23
+ describe "refuses methods inherited from Object" do
24
+ {
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")}}',
33
+ # Decorator#method_missing is public and forwards to the context, and
34
+ # method_missing is private on BasicObject so it never appeared in
35
+ # Object.instance_methods.
36
+ 'method_missing' => '{{method_missing "instance_eval" "$fs_rce_canary = :pwned"}}',
37
+ }.each do |description, template|
38
+ it "refuses #{description}" do
39
+ $fs_rce_canary = nil
40
+
41
+ expect { evaluate(template) }.to raise_error(FlavourSaver::ForbiddenMethodException)
42
+
43
+ # Assert on the side effect too, not just the exception type: a payload
44
+ # that executes *and then* raises would satisfy raise_error alone.
45
+ expect($fs_rce_canary).to be_nil
46
+ end
47
+ end
48
+
49
+ it 'refuses block expression form' do
50
+ $fs_rce_canary = nil
51
+
52
+ expect {
53
+ evaluate('{{#instance_eval "$fs_rce_canary = :pwned"}}x{{/instance_eval}}')
54
+ }.to raise_error(FlavourSaver::ForbiddenMethodException)
55
+
56
+ expect($fs_rce_canary).to be_nil
57
+ end
58
+
59
+ it 'does not shell out via send' do
60
+ expect { evaluate(%q({{send "system" "echo pwned"}})) }
61
+ .to raise_error(FlavourSaver::ForbiddenMethodException)
62
+ end
63
+
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
+ $fs_rce_canary = nil
67
+
68
+ expect { evaluate('{{instance_eval "$fs_rce_canary = :pwned"}}', ctx) }
69
+ .to raise_error(FlavourSaver::ForbiddenMethodException)
70
+
71
+ expect($fs_rce_canary).to be_nil
72
+ end
73
+ end
74
+
75
+ # Integer, Symbol and Float have no singleton class, so the visibility check
76
+ # raises TypeError rather than NameError. It must still resolve to a refusal
77
+ # (a primitive cannot carry a singleton method) rather than leaking an
78
+ # unrescued TypeError past the UnknownHelperException contract.
79
+ it 'refuses on primitive contexts without leaking TypeError' do
80
+ [5, :sym, 1.5].each do |ctx|
81
+ $fs_rce_canary = nil
82
+
83
+ expect { evaluate('{{instance_eval "$fs_rce_canary = :pwned"}}', ctx) }
84
+ .to raise_error(FlavourSaver::ForbiddenMethodException)
85
+
86
+ expect($fs_rce_canary).to be_nil
87
+ end
88
+ end
89
+
90
+ # The private forbidden_method? predicate must be correct on its own, not
91
+ # only behind the decorator wrapping in evaluate_call, since a primitive
92
+ # 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
96
+ # ...but leaves a primitive's own domain method dispatchable.
97
+ expect(runtime.send(:forbidden_method?, 5, 'bit_length')).to be false
98
+ end
99
+
100
+ it 'is catchable as UnknownHelperException, for existing rescue clauses' do
101
+ expect { evaluate('{{instance_eval "1"}}') }
102
+ .to raise_error(FlavourSaver::UnknownHelperException)
103
+ end
104
+
105
+ # Helpers.decorate_with only mixes in the helpers named in the runtime's
106
+ # helper list when that list is non-empty. A guard that consulted the global
107
+ # registry would exempt a name that was never mixed in, and dispatch would
108
+ # reach Object's implementation of it.
109
+ it 'refuses a registered helper name that was scoped out of this runtime' do
110
+ $fs_rce_canary = nil
111
+ FlavourSaver::Helpers.register_helper(:send) { 'helper!' }
112
+
113
+ ast = FlavourSaver.parse(FlavourSaver.lex('{{send "instance_eval" "$fs_rce_canary = :pwned"}}'))
114
+
115
+ expect { FlavourSaver::Runtime.new(ast, context, {}, [:this]).to_s }
116
+ .to raise_error(FlavourSaver::ForbiddenMethodException)
117
+
118
+ expect($fs_rce_canary).to be_nil
119
+ ensure
120
+ FlavourSaver::Helpers.deregister_helper(:send)
121
+ end
122
+
123
+ # Private Kernel methods are not refused by name -- doing so would break the
124
+ # ~60 context methods that share a name with one (format, open, select...).
125
+ # They are unreachable instead: public_send cannot call them, so they fall
126
+ # through to Decorator#method_missing, which also uses public_send. These
127
+ # assert non-execution rather than an exception type, because which error
128
+ # surfaces depends on the context object.
129
+ describe "private Kernel methods are unreachable" do
130
+ def expect_no_execution(template, ctx)
131
+ canary = File.join(Dir.tmpdir, "fs_rce_#{Process.pid}_#{rand(1 << 32)}")
132
+ begin
133
+ evaluate(format(template, canary), ctx)
134
+ rescue StandardError
135
+ # An exception is an acceptable outcome; execution is not.
136
+ end
137
+ expect(File.exist?(canary)).to be false
138
+ ensure
139
+ FileUtils.rm_f(canary)
140
+ end
19
141
 
20
- it 'renders correctly' do
21
- expect(Kernel).not_to receive(:eval)
22
- expect { subject }.to raise_error(RuntimeError)
142
+ %w[system eval exec fork spawn require load syscall].each do |name|
143
+ it "does not execute #{name}" do
144
+ expect_no_execution(%({{#{name} "touch %s"}}), context)
145
+ end
146
+ end
147
+
148
+ # The OSVDB-110796 payload: a proxy or delegator answering true to
149
+ # 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
+ expect_no_execution('{{system "touch %s"}}', permissive)
153
+ end
154
+
155
+ it 'does not execute via the send trampoline' do
156
+ expect_no_execution('{{send "system" "touch %s"}}', context)
157
+ end
158
+ end
159
+
160
+ # Object#try forwards to public_send, so a guard keyed on the method's owner
161
+ # being Object/Kernel/BasicObject would miss it: ActiveSupport mixes try into
162
+ # Object via its own module.
163
+ it 'refuses methods mixed into Object by a framework' do
164
+ $fs_rce_canary = nil
165
+
166
+ expect { evaluate('{{try "instance_eval" "$fs_rce_canary = :pwned"}}') }
167
+ .to raise_error(FlavourSaver::ForbiddenMethodException)
168
+
169
+ expect($fs_rce_canary).to be_nil
170
+ end
23
171
  end
24
- end
25
172
 
173
+ # Named coverage of the dangerous surface. These are behavioural rather than
174
+ # assertions about an internal list, so they keep their meaning regardless of
175
+ # how the guard is implemented.
176
+ describe "refuses each dangerous name individually" do
177
+ # No predicate names here: the lexer's IDENT rule is /([A-Za-z_]\w*)/, so a
178
+ # trailing ? cannot form part of a template identifier.
179
+ %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|
182
+ it "refuses #{name}" do
183
+ expect { evaluate("{{#{name}}}") }
184
+ .to raise_error(FlavourSaver::ForbiddenMethodException)
185
+ end
186
+ end
187
+ end
188
+
189
+ # Templates loaded from disk go through Tilt and FlavourSaver::Template rather
190
+ # than FlavourSaver.evaluate, so the refusal is asserted on that path too. This
191
+ # is what the original specs were reaching for: they passed the template *source*
192
+ # to Tilt.new, which expects a filename, so no engine matched and the whole
193
+ # suite short-circuited. Point it at a real .hbs fixture, as the other
194
+ # fixture specs do.
195
+ describe "refuses payloads in templates loaded from disk" do
196
+ let(:fixture) { File.expand_path('../../fixtures/rce.hbs', __FILE__) }
197
+
198
+ it 'refuses when rendered through Tilt' do
199
+ $fs_rce_canary = nil
200
+
201
+ expect { Tilt.new(fixture).render(context) }
202
+ .to raise_error(FlavourSaver::ForbiddenMethodException)
26
203
 
204
+ expect($fs_rce_canary).to be_nil
205
+ end
206
+
207
+ it 'has a fixture that really does contain a payload' do
208
+ expect(File.read(fixture)).to include 'instance_eval'
209
+ end
210
+ end
211
+
212
+ # The fix must not be so broad that it breaks ordinary templates.
213
+ 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'
217
+ end
218
+
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'
222
+ end
223
+
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'
227
+ ensure
228
+ FlavourSaver::Helpers.deregister_helper(:hash)
229
+ end
230
+
231
+ it 'allows a local to shadow an Object method' do
232
+ runtime = FlavourSaver::Runtime.new(
233
+ FlavourSaver.parse(FlavourSaver.lex('{{hash}}')),
234
+ context,
235
+ { 'hash' => proc { 'abc123' } }
236
+ )
237
+ expect(runtime.to_s).to eq 'abc123'
238
+ end
239
+
240
+ it 'still renders nothing for an unknown, non-forbidden method' do
241
+ expect(evaluate('{{no_such_method}}')).to eq ''
242
+ end
243
+
244
+ # Roughly sixty lexable names collide with a private Kernel method. They are
245
+ # plausible domain names and dispatched fine before 4.0.1, so refusing them
246
+ # would be a breaking change smuggled into a security patch.
247
+ %w[format open select print test load p raise loop gets rand warn catch].each do |name|
248
+ it "dispatches a context method named #{name}" do
249
+ klass = Class.new { define_method(name) { "value-#{name}" } }
250
+ expect(evaluate("{{#{name}}}", klass.new)).to eq "value-#{name}"
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1 @@
1
+ {{instance_eval "$fs_rce_canary = :pwned"}}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flavour_saver
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clayton Passmore
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-04-09 00:00:00.000000000 Z
12
+ date: 2026-08-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -133,6 +133,7 @@ files:
133
133
  - spec/fixtures/multi_level_with.hbs
134
134
  - spec/fixtures/one_character_identifier.hbs
135
135
  - spec/fixtures/raw.hbs
136
+ - spec/fixtures/rce.hbs
136
137
  - spec/fixtures/sections.hbs
137
138
  - spec/fixtures/simple_expression.hbs
138
139
  - spec/fixtures/unless.hbs
@@ -193,6 +194,7 @@ test_files:
193
194
  - spec/fixtures/multi_level_with.hbs
194
195
  - spec/fixtures/one_character_identifier.hbs
195
196
  - spec/fixtures/raw.hbs
197
+ - spec/fixtures/rce.hbs
196
198
  - spec/fixtures/sections.hbs
197
199
  - spec/fixtures/simple_expression.hbs
198
200
  - spec/fixtures/unless.hbs