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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +14 -0
- data/.standard.yml +1 -0
- data/CHANGELOG.md +57 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +48 -5
- data/README.md +153 -271
- data/flavour_saver.gemspec +17 -19
- data/lib/flavour_saver/error.rb +5 -0
- data/lib/flavour_saver/helpers.rb +19 -18
- data/lib/flavour_saver/lexer.rb +123 -50
- data/lib/flavour_saver/nodes.rb +107 -19
- data/lib/flavour_saver/parser.rb +339 -161
- data/lib/flavour_saver/partial.rb +4 -6
- data/lib/flavour_saver/rails_partial.rb +10 -4
- data/lib/flavour_saver/runtime.rb +76 -81
- data/lib/flavour_saver/template.rb +3 -5
- data/lib/flavour_saver/version.rb +1 -1
- data/lib/flavour_saver.rb +17 -17
- data/spec/acceptance/backtrack_spec.rb +9 -9
- data/spec/acceptance/comment_spec.rb +7 -7
- data/spec/acceptance/custom_block_helper_spec.rb +12 -12
- data/spec/acceptance/custom_helper_spec.rb +8 -8
- data/spec/acceptance/ensure_no_rce_spec.rb +64 -53
- data/spec/acceptance/handlebars_qunit_spec.rb +295 -297
- data/spec/acceptance/if_else_spec.rb +6 -6
- data/spec/acceptance/multi_level_with_spec.rb +10 -11
- data/spec/acceptance/nesting_depth_spec.rb +34 -0
- data/spec/acceptance/one_character_identifier_spec.rb +8 -8
- data/spec/acceptance/raw_block_spec.rb +7 -7
- data/spec/acceptance/sections_spec.rb +17 -14
- data/spec/acceptance/segment_literals_spec.rb +5 -5
- data/spec/acceptance/simple_expression_spec.rb +9 -9
- data/spec/acceptance/subexpression_spec.rb +18 -17
- data/spec/acceptance/unless_spec.rb +19 -6
- data/spec/lib/flavour_saver/lexer_spec.rb +103 -89
- data/spec/lib/flavour_saver/nodes_spec.rb +85 -0
- data/spec/lib/flavour_saver/parser_spec.rb +155 -95
- data/spec/lib/flavour_saver/runtime_spec.rb +94 -96
- data/spec/lib/flavour_saver/template_spec.rb +1 -2
- metadata +19 -48
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
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
|
|
8
|
-
require
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
60
|
-
expect { evaluate(
|
|
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
|
|
65
|
-
[Object.new, {
|
|
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
|
|
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
|
|
94
|
-
runtime = FlavourSaver::Runtime.new(FlavourSaver.parse(FlavourSaver.lex(
|
|
95
|
-
expect(runtime.send(:forbidden_method?, 5,
|
|
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,
|
|
99
|
+
expect(runtime.send(:forbidden_method?, 5, "bit_length")).to be false
|
|
98
100
|
end
|
|
99
101
|
|
|
100
|
-
it
|
|
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
|
|
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) {
|
|
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
|
|
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
|
|
151
|
-
permissive = Class.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
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
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(
|
|
202
|
+
let(:fixture) { File.expand_path("../../fixtures/rce.hbs", __FILE__) }
|
|
197
203
|
|
|
198
|
-
it
|
|
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
|
|
208
|
-
expect(File.read(fixture)).to include
|
|
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
|
|
215
|
-
klass = Class.new {
|
|
216
|
-
|
|
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
|
|
220
|
-
expect(evaluate(
|
|
221
|
-
expect(evaluate(
|
|
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
|
|
225
|
-
FlavourSaver::Helpers.register_helper(:hash) {
|
|
226
|
-
expect(evaluate(
|
|
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
|
|
241
|
+
it "allows a local to shadow an Object method" do
|
|
232
242
|
runtime = FlavourSaver::Runtime.new(
|
|
233
|
-
FlavourSaver.parse(FlavourSaver.lex(
|
|
243
|
+
FlavourSaver.parse(FlavourSaver.lex("{{hash}}")),
|
|
234
244
|
context,
|
|
235
|
-
{
|
|
245
|
+
{"hash" => proc { "abc123" }}
|
|
236
246
|
)
|
|
237
|
-
expect(runtime.to_s).to eq
|
|
247
|
+
expect(runtime.to_s).to eq "abc123"
|
|
238
248
|
end
|
|
239
249
|
|
|
240
|
-
it
|
|
241
|
-
expect(evaluate(
|
|
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
|