safemode 1.5.0 → 2.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/demo.rb +2 -0
- data/lib/safemode/core_jails.rb +18 -21
- data/lib/safemode/parser.rb +57 -20
- data/lib/safemode.rb +2 -11
- data/safemode.gemspec +10 -10
- data/test/test_erb_eval.rb +4 -4
- data/test/test_helper.rb +5 -1
- data/test/test_jail.rb +2 -0
- data/test/test_safemode_eval.rb +131 -4
- data/test/test_safemode_parser.rb +103 -0
- metadata +35 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31ed45049398953b35fafe5ff837df2332712caa9534c5278e971d9031a61836
|
|
4
|
+
data.tar.gz: 49b507273a0ef6d03578be21a26f26388fb4cd54d253670a8932ae7891ae40f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eefb3d5121ad93910e1468cd635a5d77320dfbd23c341469af7acc1d90c80947ab1f8ec058bf1775b9269566d2503cfec0118bb6143e9d35ee6ba5afbb055caa
|
|
7
|
+
data.tar.gz: ab5f0385d055999f6a6463381d24494bab377fe12aebec639b3be5fc78faf4355c5485105ba6c73bbc0a0023c2db3dd063bc35abee19d4564748d15a925650d8
|
data/demo.rb
CHANGED
data/lib/safemode/core_jails.rb
CHANGED
|
@@ -17,15 +17,11 @@ module Safemode
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def core_classes
|
|
20
|
-
klasses = [ Array, Float, Hash, Range, String, Symbol, Time, NilClass, FalseClass, TrueClass ]
|
|
20
|
+
klasses = [ Array, Float, Hash, Integer, Range, String, Symbol, Time, NilClass, FalseClass, TrueClass ]
|
|
21
21
|
klasses << Date if defined? Date
|
|
22
22
|
klasses << DateTime if defined? DateTime
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
else
|
|
26
|
-
klasses << Bignum
|
|
27
|
-
klasses << Fixnum
|
|
28
|
-
end
|
|
23
|
+
klasses << Data if defined? Data # Ruby 3.2 addition
|
|
24
|
+
klasses << Set if defined? Set # Ruby 3.2 addition
|
|
29
25
|
klasses
|
|
30
26
|
end
|
|
31
27
|
|
|
@@ -46,31 +42,21 @@ module Safemode
|
|
|
46
42
|
# whitelisted methods for core classes ... kind of arbitrary selection
|
|
47
43
|
@@methods_whitelist = {
|
|
48
44
|
'Array' => %w(any? assoc at blank? collect collect! compact compact!
|
|
49
|
-
concat delete delete_at delete_if each each_index
|
|
50
|
-
fetch fill first flatten flatten! hash include? index
|
|
45
|
+
concat delete delete_at delete_if each each_index each_with_index
|
|
46
|
+
empty? fetch fill first flatten flatten! hash include? index
|
|
51
47
|
indexes indices inject insert join last length map map! max min
|
|
52
48
|
nitems pop push present? rassoc reject reject! reverse
|
|
53
49
|
reverse! reverse_each rindex select shift size slice
|
|
54
50
|
slice! sort sort! transpose to_sentence uniq uniq! unshift
|
|
55
51
|
values_at zip),
|
|
56
52
|
|
|
57
|
-
'Bignum' => %w(abs blank? ceil chr coerce div divmod downto floor hash
|
|
58
|
-
integer? modulo next nonzero? present? quo remainder round
|
|
59
|
-
singleton_method_added size step succ times to_f to_i
|
|
60
|
-
to_int to_s truncate upto zero?),
|
|
61
|
-
|
|
62
|
-
'Fixnum' => %w(abs blank? ceil chr coerce div divmod downto floor id2name
|
|
63
|
-
integer? modulo modulo next nonzero? present? quo remainder
|
|
64
|
-
round singleton_method_added size step succ times to_f to_i
|
|
65
|
-
to_int to_s to_sym truncate upto zero?),
|
|
66
|
-
|
|
67
53
|
'Float' => %w(abs blank? ceil coerce div divmod finite? floor hash
|
|
68
54
|
infinite? integer? modulo nan? nonzero? present? quo
|
|
69
55
|
remainder round singleton_method_added step to_f to_i
|
|
70
56
|
to_int to_s truncate zero?),
|
|
71
57
|
|
|
72
|
-
'Hash' => %w(any? blank? clear delete delete_if each each_key
|
|
73
|
-
|
|
58
|
+
'Hash' => %w(any? blank? clear delete delete_if each each_key each_pair
|
|
59
|
+
each_value each_with_index empty? fetch dig has_key? has_value?
|
|
74
60
|
include? index invert key? keys length member? merge merge!
|
|
75
61
|
present? rec_merge! rehash reject reject! select shift
|
|
76
62
|
size sort store update value? values values_at),
|
|
@@ -116,6 +102,17 @@ module Safemode
|
|
|
116
102
|
'DateTime' => %w(blank? hour min new_offset newof of offset present? sec
|
|
117
103
|
sec_fraction strftime to_datetime_default_s to_json zone),
|
|
118
104
|
|
|
105
|
+
'Data' => %w(deconstruct deconstruct_keys hash members),
|
|
106
|
+
|
|
107
|
+
'Set' => %w(any? blank? clear clone collect count delete delete?
|
|
108
|
+
delete_if difference disjoint? each each_with_index
|
|
109
|
+
each_with_object empty? filter find first flatten
|
|
110
|
+
flatten! hash include? inject intersect? intersection
|
|
111
|
+
join keep_if length map max member? merge min
|
|
112
|
+
present? reject reject! replace select select!
|
|
113
|
+
size sort sort_by subset? superset? union
|
|
114
|
+
uniq zip),
|
|
115
|
+
|
|
119
116
|
'NilClass' => %w(blank? duplicable? present? to_f to_i),
|
|
120
117
|
|
|
121
118
|
'FalseClass' => %w(blank? duplicable? present?),
|
data/lib/safemode/parser.rb
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
module Safemode
|
|
2
2
|
class Parser < Ruby2Ruby
|
|
3
|
-
# @@parser = defined?(RubyParser) ? 'RubyParser' : 'ParseTree'
|
|
4
|
-
@@parser = 'RubyParser'
|
|
5
|
-
|
|
6
3
|
class << self
|
|
7
4
|
def jail(code, allowed_fcalls = [])
|
|
8
5
|
@@allowed_fcalls = allowed_fcalls
|
|
@@ -11,18 +8,7 @@ module Safemode
|
|
|
11
8
|
end
|
|
12
9
|
|
|
13
10
|
def parse(code)
|
|
14
|
-
|
|
15
|
-
# when 'ParseTree'
|
|
16
|
-
# ParseTree.translate(code)
|
|
17
|
-
when 'RubyParser'
|
|
18
|
-
RubyParser.new.parse(code)
|
|
19
|
-
else
|
|
20
|
-
raise "unknown parser #{@@parser}"
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def parser=(parser)
|
|
25
|
-
@@parser = parser
|
|
11
|
+
Prism::Translation::RubyParser.parse(code)
|
|
26
12
|
end
|
|
27
13
|
end
|
|
28
14
|
|
|
@@ -89,8 +75,13 @@ module Safemode
|
|
|
89
75
|
# :colon2 is used for module constants
|
|
90
76
|
:colon2,
|
|
91
77
|
# unnecessarily advanced?
|
|
92
|
-
:argscat, :argspush, :splat,
|
|
93
|
-
|
|
78
|
+
:argscat, :argspush, :splat, :kwsplat,
|
|
79
|
+
# NOTE: op_asgn*/safe_op_asgn* do not insert .to_jail on the
|
|
80
|
+
# receiver. Setters are called directly on the real object, bypassing Jail.
|
|
81
|
+
:op_asgn, :op_asgn1, :op_asgn2, :op_asgn_and, :op_asgn_or,
|
|
82
|
+
:safe_op_asgn, :safe_op_asgn2,
|
|
83
|
+
# pattern matching (Ruby 3.0+)
|
|
84
|
+
:in, :array_pat, :hash_pat, :find_pat, :kwrest,
|
|
94
85
|
# needed for haml
|
|
95
86
|
:block ]
|
|
96
87
|
|
|
@@ -98,18 +89,21 @@ module Safemode
|
|
|
98
89
|
# see below for :const handling
|
|
99
90
|
:defn, :defs, :alias, :valias, :undef, :class, :attrset,
|
|
100
91
|
:module, :sclass, :colon3,
|
|
101
|
-
:fbody, :scope, :block_arg, :postexe,
|
|
92
|
+
:fbody, :scope, :block_arg, :postexe, :preexe,
|
|
102
93
|
:redo, :retry, :begin, :rescue, :resbody, :ensure,
|
|
103
94
|
:defined, :super, :zsuper, :return,
|
|
104
95
|
:dmethod, :bmethod, :to_ary, :svalue, :match,
|
|
105
|
-
:attrasgn, :
|
|
96
|
+
:attrasgn, :safe_attrasgn,
|
|
97
|
+
:cdecl, :cvasgn, :cvdecl, :cvar, :gvar, :gasgn,
|
|
106
98
|
:xstr, :dxstr,
|
|
107
99
|
# not sure how secure ruby regexp is, so leave it out for now
|
|
108
100
|
:dregx, :dregx_once, :match2, :match3, :nth_ref, :back_ref,
|
|
109
101
|
# block_pass represents &:method, which would bypass the whitelist e.g. by array.each(&:destroy)
|
|
110
102
|
# at this point we don't know the receiver so we rather disable it completely,
|
|
111
103
|
# use array.each { |item| item.destroy } instead
|
|
112
|
-
:block_pass
|
|
104
|
+
:block_pass,
|
|
105
|
+
# lambda creates Proc objects
|
|
106
|
+
:lambda ]
|
|
113
107
|
|
|
114
108
|
# SexpProcessor bails when we overwrite these ... but they are listed as
|
|
115
109
|
# "internal nodes that you can't get to" in sexp_processor.rb
|
|
@@ -207,6 +201,49 @@ module Safemode
|
|
|
207
201
|
end
|
|
208
202
|
end
|
|
209
203
|
|
|
204
|
+
# Ruby2Ruby bug: __var adds ^ (pin) to all lvars inside :in context,
|
|
205
|
+
# including the body after "then" where ^ is invalid syntax.
|
|
206
|
+
# Fix: process body outside the :in context.
|
|
207
|
+
def process_in(exp)
|
|
208
|
+
_, pattern, *body = exp
|
|
209
|
+
|
|
210
|
+
guard = extract_guard(pattern)
|
|
211
|
+
cond = process pattern
|
|
212
|
+
body = body.compact.map { |sexp|
|
|
213
|
+
in_context :in_body do
|
|
214
|
+
indent process sexp
|
|
215
|
+
end
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
body << indent("# do nothing") if body.empty?
|
|
219
|
+
body = body.join "\n"
|
|
220
|
+
|
|
221
|
+
header = "in #{cond}"
|
|
222
|
+
header << " #{guard}" if guard
|
|
223
|
+
"#{header} then\n#{body.chomp}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Prism translates guard clauses (in pattern if cond / in pattern unless cond)
|
|
227
|
+
# as s(:if, guard, pattern, nil) / s(:if, guard, nil, pattern). We detect this
|
|
228
|
+
# and rewrite the sexp in-place so process_if renders only the pattern, then
|
|
229
|
+
# return the guard string for process_in to append.
|
|
230
|
+
def extract_guard(pattern)
|
|
231
|
+
return unless pattern.sexp_type == :if
|
|
232
|
+
|
|
233
|
+
_, guard_sexp, if_body, else_body = pattern
|
|
234
|
+
if if_body && !else_body
|
|
235
|
+
guard_str = in_context(:in_body) { "if #{process guard_sexp.deep_clone}" }
|
|
236
|
+
pattern.clear
|
|
237
|
+
if_body.each { |node| pattern << node }
|
|
238
|
+
elsif else_body && !if_body
|
|
239
|
+
guard_str = in_context(:in_body) { "unless #{process guard_sexp.deep_clone}" }
|
|
240
|
+
pattern.clear
|
|
241
|
+
else_body.each { |node| pattern << node }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
guard_str
|
|
245
|
+
end
|
|
246
|
+
|
|
210
247
|
# Ruby2Ruby process_if rewrites if and unless statements in a way that
|
|
211
248
|
# makes the result unusable for evaluation in, e.g. ERB which appends a
|
|
212
249
|
# call to to_s when using <%= %> tags. We'd need to either enclose the
|
data/lib/safemode.rb
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
2
|
|
|
3
3
|
require 'ruby2ruby'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
rescue LoadError => e
|
|
7
|
-
end
|
|
8
|
-
# this doesn't work somehow. Maybe something changed inside
|
|
9
|
-
# ParseTree or sexp_processor or so.
|
|
10
|
-
# (the require itself works, but ParseTree doesn't play nice)
|
|
11
|
-
# begin
|
|
12
|
-
# require 'parse_tree'
|
|
13
|
-
# rescue LoadError => e
|
|
14
|
-
# end
|
|
4
|
+
require 'prism'
|
|
5
|
+
require 'prism/translation/ruby_parser'
|
|
15
6
|
|
|
16
7
|
require 'safemode/core_ext'
|
|
17
8
|
require 'safemode/blankslate'
|
data/safemode.gemspec
CHANGED
|
@@ -4,10 +4,10 @@ require 'date'
|
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |s|
|
|
6
6
|
s.name = "safemode".freeze
|
|
7
|
-
s.version = "
|
|
7
|
+
s.version = "2.0.0"
|
|
8
8
|
s.date = Date.today
|
|
9
9
|
|
|
10
|
-
s.summary = "A library for safe evaluation of Ruby code based on
|
|
10
|
+
s.summary = "A library for safe evaluation of Ruby code based on Prism and Ruby2Ruby"
|
|
11
11
|
s.description = "A library for safe evaluation of Ruby code based on RubyParser and Ruby2Ruby. Provides Rails ActionView template handlers for ERB and Haml."
|
|
12
12
|
s.homepage = "https://github.com/svenfuchs/safemode"
|
|
13
13
|
s.licenses = ["MIT"]
|
|
@@ -52,14 +52,14 @@ Gem::Specification.new do |s|
|
|
|
52
52
|
"test/test_safemode_parser.rb"
|
|
53
53
|
]
|
|
54
54
|
|
|
55
|
-
s.required_ruby_version = ">=
|
|
55
|
+
s.required_ruby_version = ">= 3.0", "< 3.4"
|
|
56
56
|
|
|
57
|
-
s.add_runtime_dependency "ruby2ruby", "
|
|
58
|
-
s.add_runtime_dependency "
|
|
59
|
-
s.add_runtime_dependency "sexp_processor", "
|
|
57
|
+
s.add_runtime_dependency "ruby2ruby", "~> 2.4"
|
|
58
|
+
s.add_runtime_dependency "prism", "~> 1.0"
|
|
59
|
+
s.add_runtime_dependency "sexp_processor", "~> 4.10"
|
|
60
60
|
|
|
61
|
-
s.add_development_dependency "rake"
|
|
62
|
-
s.add_development_dependency "rdoc"
|
|
63
|
-
s.add_development_dependency "simplecov"
|
|
64
|
-
s.add_development_dependency "test-unit"
|
|
61
|
+
s.add_development_dependency "rake", "~> 13.4"
|
|
62
|
+
s.add_development_dependency "rdoc", "~> 7.2"
|
|
63
|
+
s.add_development_dependency "simplecov", "~> 0.22"
|
|
64
|
+
s.add_development_dependency "test-unit", "~> 3.7"
|
|
65
65
|
end
|
data/test/test_erb_eval.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require File.join(File.dirname(__FILE__), 'test_helper')
|
|
2
4
|
|
|
3
5
|
class TestERBEval < Test::Unit::TestCase
|
|
@@ -58,19 +60,17 @@ class TestERBEval < Test::Unit::TestCase
|
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
TestHelper.no_method_error_raising_calls.each do |call|
|
|
61
|
-
call.gsub!('"', '\\\\"')
|
|
62
63
|
class_eval %Q(
|
|
63
64
|
def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_no_method
|
|
64
|
-
assert_raise_no_method "#{call}", @assigns, @locals
|
|
65
|
+
assert_raise_no_method "#{call.gsub('"', '\\\\"')}", @assigns, @locals
|
|
65
66
|
end
|
|
66
67
|
)
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
TestHelper.security_error_raising_calls.each do |call|
|
|
70
|
-
call.gsub!('"', '\\\\"')
|
|
71
71
|
class_eval %Q(
|
|
72
72
|
def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_security
|
|
73
|
-
assert_raise_security "#{call}", @assigns, @locals
|
|
73
|
+
assert_raise_security "#{call.gsub('"', '\\\\"')}", @assigns, @locals
|
|
74
74
|
end
|
|
75
75
|
)
|
|
76
76
|
end
|
data/test/test_helper.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
if ENV['COVERAGE']
|
|
2
4
|
require 'simplecov'
|
|
3
5
|
SimpleCov.start {add_filter 'test_'}
|
|
@@ -110,6 +112,8 @@ class Article
|
|
|
110
112
|
Comment
|
|
111
113
|
end
|
|
112
114
|
|
|
115
|
+
attr_accessor :status
|
|
116
|
+
|
|
113
117
|
def method_with_kwargs(a_keyword: false)
|
|
114
118
|
a_keyword
|
|
115
119
|
end
|
|
@@ -148,7 +152,7 @@ class Comment
|
|
|
148
152
|
end
|
|
149
153
|
|
|
150
154
|
class Article::Jail < Safemode::Jail
|
|
151
|
-
allow :title, :comments, :is_article?, :comment_class, :method_with_kwargs
|
|
155
|
+
allow :title, :comments, :is_article?, :comment_class, :method_with_kwargs, :status
|
|
152
156
|
|
|
153
157
|
def author_name
|
|
154
158
|
"this article's author name"
|
data/test/test_jail.rb
CHANGED
data/test/test_safemode_eval.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require File.join(File.dirname(__FILE__), 'test_helper')
|
|
2
4
|
|
|
3
5
|
class TestSafemodeEval < Test::Unit::TestCase
|
|
@@ -92,26 +94,151 @@ class TestSafemodeEval < Test::Unit::TestCase
|
|
|
92
94
|
assert @box.eval("@article.method_with_kwargs(a_keyword: true)", @assigns)
|
|
93
95
|
end
|
|
94
96
|
|
|
97
|
+
def test_pattern_matching_with_literal
|
|
98
|
+
assert_equal "matched", @box.eval('case 1; in 1; "matched"; in 2; "nope"; end')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_pattern_matching_with_array_destructuring
|
|
102
|
+
assert_equal 1, @box.eval('case [1, 2, 3]; in [a, b, c]; a; end')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_pattern_matching_with_hash_destructuring
|
|
106
|
+
assert_equal 1, @box.eval('case({a: 1, b: 2}); in {a:}; a; end')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_pattern_matching_with_assign
|
|
110
|
+
assert_equal "two", @box.eval('case @val; in 1; "one"; in 2; "two"; end', { val: 2 })
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_pattern_matching_with_find_pattern
|
|
114
|
+
assert_equal "found", @box.eval('case [1, 2, 3]; in [*, 2, *]; "found"; end')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_pattern_matching_with_pin_operator
|
|
118
|
+
assert_equal "matched", @box.eval('y = 1; case 1; in ^y; "matched"; end')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_pattern_matching_with_multiple_clauses
|
|
122
|
+
assert_equal "second", @box.eval('case [3, 4]; in [1, 2]; "first"; in [3, 4]; "second"; end')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_pattern_matching_no_match_returns_nil
|
|
126
|
+
assert_nil @box.eval('case 3; in 1; "one"; in 2; "two"; else; nil; end')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_pattern_matching_with_if_guard
|
|
130
|
+
assert_equal "positive", @box.eval('case 1; in x if x > 0; "positive"; end')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_pattern_matching_with_unless_guard
|
|
134
|
+
assert_equal "not negative", @box.eval('case 1; in x unless x < 0; "not negative"; end')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_pattern_matching_guard_no_match
|
|
138
|
+
assert_nil @box.eval('case 1; in x if x < 0; "negative"; else; nil; end')
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_pattern_matching_guard_with_array_pattern
|
|
142
|
+
assert_equal "yes", @box.eval('case [1, 2]; in [a, b] if a > 0; "yes"; end')
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_pattern_matching_guard_with_hash_pattern
|
|
146
|
+
assert_equal "alice", @box.eval('case({name: "alice"}); in {name:} if name.start_with?("a"); name; end')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_rightward_assignment
|
|
150
|
+
assert_equal 1, @box.eval('[1, 2] => [a, b]; a')
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_pattern_matching_with_jailed_hash
|
|
154
|
+
assert_equal "an article title", @box.eval('case @data; in {title:}; title; end', { data: { title: "an article title" } })
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_hash_shorthand
|
|
158
|
+
# TODO: Remove the check once Ruby 3.1 is the minimum
|
|
159
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1')
|
|
160
|
+
assert_equal({ a: 1, b: 2 }, @box.eval('a = 1; b = 2; { a:, b: }'))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_endless_range
|
|
165
|
+
assert_equal [3, 4, 5], @box.eval('[1,2,3,4,5][2..]')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_beginless_range
|
|
169
|
+
assert_equal [1, 2, 3], @box.eval('[1,2,3,4,5][..2]')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# attrasgn (@article.status = val) is blocked at parse time
|
|
173
|
+
def test_attrasgn_is_blocked
|
|
174
|
+
assert_raise(Safemode::SecurityError) { @box.eval('@article.status = "new_value"', @assigns) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_safe_attrasgn_is_blocked
|
|
178
|
+
assert_raise(Safemode::SecurityError) { @box.eval('@article&.status = "new_value"', @assigns) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# op_asgn2 (@article.status ||= val) is NOT blocked at parse time.
|
|
182
|
+
# NOTE: .to_jail is not inserted on the receiver, so the setter is called
|
|
183
|
+
# directly on the real object, bypassing the Jail whitelist.
|
|
184
|
+
def test_op_asgn2_bypasses_jail
|
|
185
|
+
article = Article.new
|
|
186
|
+
assert_nil article.status
|
|
187
|
+
@box.eval('@article.status ||= "new_value"', { article: article })
|
|
188
|
+
assert_equal "new_value", article.status
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_safe_op_asgn2_bypasses_jail
|
|
192
|
+
article = Article.new
|
|
193
|
+
assert_nil article.status
|
|
194
|
+
@box.eval('@article&.status ||= "new_value"', { article: article })
|
|
195
|
+
assert_equal "new_value", article.status
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_lambda_is_blocked
|
|
199
|
+
assert_raise(Safemode::SecurityError) { @box.eval('-> { 1 }') }
|
|
200
|
+
end
|
|
201
|
+
|
|
95
202
|
def test_sending_to_method_missing
|
|
96
203
|
assert_raise_with_message(Safemode::NoMethodError, /#no_such_method/) do
|
|
97
204
|
@box.eval("@article.no_such_method('arg', key: 'value')", @assigns)
|
|
98
205
|
end
|
|
99
206
|
end
|
|
100
207
|
|
|
208
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
|
|
209
|
+
def test_pattern_matching_with_data_subclass
|
|
210
|
+
point_class = Data.define(:x, :y)
|
|
211
|
+
point = point_class.new(x: 10, y: 20)
|
|
212
|
+
assert_equal 10, @box.eval('case @point; in {x:}; x; end', { point: point })
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def test_data_subclass_inherits_data_jail
|
|
216
|
+
point_class = Data.define(:x, :y)
|
|
217
|
+
point = point_class.new(x: 10, y: 20)
|
|
218
|
+
assert_equal [:x, :y], @box.eval('@point.members', { point: point })
|
|
219
|
+
assert_equal [10, 20], @box.eval('@point.deconstruct', { point: point })
|
|
220
|
+
assert_equal({ x: 10 }, @box.eval('@point.deconstruct_keys([:x])', { point: point }))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_data_subclass_jail_blocks_non_whitelisted_methods
|
|
224
|
+
point_class = Data.define(:x, :y)
|
|
225
|
+
point = point_class.new(x: 10, y: 20)
|
|
226
|
+
assert_raise(Safemode::NoMethodError) { @box.eval('@point.instance_variables', { point: point }) }
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
101
230
|
TestHelper.no_method_error_raising_calls.each do |call|
|
|
102
|
-
call.gsub!('"', '\\\\"')
|
|
103
231
|
class_eval %Q(
|
|
104
232
|
def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_no_method
|
|
105
|
-
assert_raise_no_method "#{call}", @assigns, @locals
|
|
233
|
+
assert_raise_no_method "#{call.gsub('"', '\\\\"')}", @assigns, @locals
|
|
106
234
|
end
|
|
107
235
|
)
|
|
108
236
|
end
|
|
109
237
|
|
|
110
238
|
TestHelper.security_error_raising_calls.each do |call|
|
|
111
|
-
call.gsub!('"', '\\\\"')
|
|
112
239
|
class_eval %Q(
|
|
113
240
|
def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_security
|
|
114
|
-
assert_raise_security "#{call}", @assigns, @locals
|
|
241
|
+
assert_raise_security "#{call.gsub('"', '\\\\"')}", @assigns, @locals
|
|
115
242
|
end
|
|
116
243
|
)
|
|
117
244
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require File.join(File.dirname(__FILE__), 'test_helper')
|
|
2
4
|
|
|
3
5
|
class TestSafemodeParser < Test::Unit::TestCase
|
|
@@ -65,6 +67,107 @@ class TestSafemodeParser < Test::Unit::TestCase
|
|
|
65
67
|
end
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def test_safe_attrasgn_is_disabled
|
|
71
|
+
assert_raise Safemode::SecurityError do
|
|
72
|
+
jail('@article&.title = "new_value"')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_safe_op_asgn2_is_allowed
|
|
77
|
+
assert_nothing_raised do
|
|
78
|
+
jail('@article&.title ||= "new_value"')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_lambda_is_disabled
|
|
83
|
+
assert_raise Safemode::SecurityError do
|
|
84
|
+
jail('-> { 1 }')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_case_in_with_literal
|
|
89
|
+
jailed = jail("case x; in 1; \"one\"; in 2; \"two\"; end")
|
|
90
|
+
assert_match(/in 1 then/, jailed)
|
|
91
|
+
assert_match(/in 2 then/, jailed)
|
|
92
|
+
assert_match(/to_jail\.x/, jailed)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_case_in_with_array_pattern
|
|
96
|
+
jailed = jail("case x; in [a, b]; a; end")
|
|
97
|
+
assert_match(/in \[a, b\] then/, jailed)
|
|
98
|
+
assert_no_match(/\^a/, jailed)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_case_in_with_hash_pattern
|
|
102
|
+
jailed = jail("case x; in {name:}; name; end")
|
|
103
|
+
assert_match(/in \{ name: \} then/, jailed)
|
|
104
|
+
assert_no_match(/\^name/, jailed)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_case_in_with_find_pattern
|
|
108
|
+
jailed = jail("case x; in [*, 2, *]; \"found\"; end")
|
|
109
|
+
assert_match(/in \[\*, 2, \*\] then/, jailed)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_case_in_pin_operator
|
|
113
|
+
jailed = jail("y = 1; case x; in ^y; true; end")
|
|
114
|
+
assert_match(/in \^y then/, jailed)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_case_in_body_does_not_pin_variables
|
|
118
|
+
jailed = jail("case x; in [a, b]; a; end")
|
|
119
|
+
lines = jailed.lines
|
|
120
|
+
body_start = lines.index { |l| l.match?(/^in \[/) } + 1
|
|
121
|
+
lines[body_start..].each { |line| assert_no_match(/\^[a-z]/, line) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_case_in_multiple_clauses
|
|
125
|
+
jailed = jail("case x; in [a, b]; a; in {c:}; c; end")
|
|
126
|
+
assert_match(/in \[a, b\] then/, jailed)
|
|
127
|
+
assert_match(/in \{ c: \} then/, jailed)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_case_in_with_if_guard
|
|
131
|
+
jailed = jail("case x; in 1 if true; \"matched\"; end")
|
|
132
|
+
assert_match(/in 1 if true then/, jailed)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_case_in_with_unless_guard
|
|
136
|
+
jailed = jail("case x; in 1 unless false; \"matched\"; end")
|
|
137
|
+
assert_match(/in 1 unless false then/, jailed)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_case_in_guard_does_not_pin_variables
|
|
141
|
+
jailed = jail("case x; in [a, b] if a; a; end")
|
|
142
|
+
guard_line = jailed.lines.find { |l| l.match?(/^in \[/) }
|
|
143
|
+
assert_match(/if a then/, guard_line)
|
|
144
|
+
assert_no_match(/\^a/, guard_line)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def test_case_in_guard_jails_method_calls
|
|
148
|
+
jailed = jail('case x; in {name: n} if n.start_with?("a"); n; end')
|
|
149
|
+
assert_match(/if n\.to_jail\.start_with\?/, jailed)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_rightward_assignment
|
|
153
|
+
jailed = jail("x => a")
|
|
154
|
+
assert_match(/case to_jail\.x/, jailed)
|
|
155
|
+
assert_match(/in a then/, jailed)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_hash_shorthand_in_literal
|
|
159
|
+
jailed = jail("a = 1; b = 2; { a:, b: }")
|
|
160
|
+
assert_match(/a:,/, jailed)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_endless_range
|
|
164
|
+
assert_jailed "(1..)", "(1..)"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_beginless_range
|
|
168
|
+
assert_jailed "(..5)", "(..5)"
|
|
169
|
+
end
|
|
170
|
+
|
|
68
171
|
private
|
|
69
172
|
|
|
70
173
|
def assert_jailed(expected, code)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safemode
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sven Fuchs
|
|
@@ -10,112 +10,110 @@ authors:
|
|
|
10
10
|
- Kingsley Hendrickse
|
|
11
11
|
- Ohad Levy
|
|
12
12
|
- Dmitri Dolguikh
|
|
13
|
-
autorequire:
|
|
14
13
|
bindir: bin
|
|
15
14
|
cert_chain: []
|
|
16
|
-
date:
|
|
15
|
+
date: 2026-05-21 00:00:00.000000000 Z
|
|
17
16
|
dependencies:
|
|
18
17
|
- !ruby/object:Gem::Dependency
|
|
19
18
|
name: ruby2ruby
|
|
20
19
|
requirement: !ruby/object:Gem::Requirement
|
|
21
20
|
requirements:
|
|
22
|
-
- - "
|
|
21
|
+
- - "~>"
|
|
23
22
|
- !ruby/object:Gem::Version
|
|
24
|
-
version: 2.4
|
|
23
|
+
version: '2.4'
|
|
25
24
|
type: :runtime
|
|
26
25
|
prerelease: false
|
|
27
26
|
version_requirements: !ruby/object:Gem::Requirement
|
|
28
27
|
requirements:
|
|
29
|
-
- - "
|
|
28
|
+
- - "~>"
|
|
30
29
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: 2.4
|
|
30
|
+
version: '2.4'
|
|
32
31
|
- !ruby/object:Gem::Dependency
|
|
33
|
-
name:
|
|
32
|
+
name: prism
|
|
34
33
|
requirement: !ruby/object:Gem::Requirement
|
|
35
34
|
requirements:
|
|
36
|
-
- - "
|
|
35
|
+
- - "~>"
|
|
37
36
|
- !ruby/object:Gem::Version
|
|
38
|
-
version:
|
|
37
|
+
version: '1.0'
|
|
39
38
|
type: :runtime
|
|
40
39
|
prerelease: false
|
|
41
40
|
version_requirements: !ruby/object:Gem::Requirement
|
|
42
41
|
requirements:
|
|
43
|
-
- - "
|
|
42
|
+
- - "~>"
|
|
44
43
|
- !ruby/object:Gem::Version
|
|
45
|
-
version:
|
|
44
|
+
version: '1.0'
|
|
46
45
|
- !ruby/object:Gem::Dependency
|
|
47
46
|
name: sexp_processor
|
|
48
47
|
requirement: !ruby/object:Gem::Requirement
|
|
49
48
|
requirements:
|
|
50
|
-
- - "
|
|
49
|
+
- - "~>"
|
|
51
50
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: 4.10
|
|
51
|
+
version: '4.10'
|
|
53
52
|
type: :runtime
|
|
54
53
|
prerelease: false
|
|
55
54
|
version_requirements: !ruby/object:Gem::Requirement
|
|
56
55
|
requirements:
|
|
57
|
-
- - "
|
|
56
|
+
- - "~>"
|
|
58
57
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: 4.10
|
|
58
|
+
version: '4.10'
|
|
60
59
|
- !ruby/object:Gem::Dependency
|
|
61
60
|
name: rake
|
|
62
61
|
requirement: !ruby/object:Gem::Requirement
|
|
63
62
|
requirements:
|
|
64
|
-
- - "
|
|
63
|
+
- - "~>"
|
|
65
64
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '
|
|
65
|
+
version: '13.4'
|
|
67
66
|
type: :development
|
|
68
67
|
prerelease: false
|
|
69
68
|
version_requirements: !ruby/object:Gem::Requirement
|
|
70
69
|
requirements:
|
|
71
|
-
- - "
|
|
70
|
+
- - "~>"
|
|
72
71
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '
|
|
72
|
+
version: '13.4'
|
|
74
73
|
- !ruby/object:Gem::Dependency
|
|
75
74
|
name: rdoc
|
|
76
75
|
requirement: !ruby/object:Gem::Requirement
|
|
77
76
|
requirements:
|
|
78
|
-
- - "
|
|
77
|
+
- - "~>"
|
|
79
78
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '
|
|
79
|
+
version: '7.2'
|
|
81
80
|
type: :development
|
|
82
81
|
prerelease: false
|
|
83
82
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
83
|
requirements:
|
|
85
|
-
- - "
|
|
84
|
+
- - "~>"
|
|
86
85
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '
|
|
86
|
+
version: '7.2'
|
|
88
87
|
- !ruby/object:Gem::Dependency
|
|
89
88
|
name: simplecov
|
|
90
89
|
requirement: !ruby/object:Gem::Requirement
|
|
91
90
|
requirements:
|
|
92
|
-
- - "
|
|
91
|
+
- - "~>"
|
|
93
92
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '0'
|
|
93
|
+
version: '0.22'
|
|
95
94
|
type: :development
|
|
96
95
|
prerelease: false
|
|
97
96
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
97
|
requirements:
|
|
99
|
-
- - "
|
|
98
|
+
- - "~>"
|
|
100
99
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: '0'
|
|
100
|
+
version: '0.22'
|
|
102
101
|
- !ruby/object:Gem::Dependency
|
|
103
102
|
name: test-unit
|
|
104
103
|
requirement: !ruby/object:Gem::Requirement
|
|
105
104
|
requirements:
|
|
106
|
-
- - "
|
|
105
|
+
- - "~>"
|
|
107
106
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '
|
|
107
|
+
version: '3.7'
|
|
109
108
|
type: :development
|
|
110
109
|
prerelease: false
|
|
111
110
|
version_requirements: !ruby/object:Gem::Requirement
|
|
112
111
|
requirements:
|
|
113
|
-
- - "
|
|
112
|
+
- - "~>"
|
|
114
113
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: '
|
|
114
|
+
version: '3.7'
|
|
116
115
|
description: A library for safe evaluation of Ruby code based on RubyParser and Ruby2Ruby.
|
|
117
116
|
Provides Rails ActionView template handlers for ERB and Haml.
|
|
118
|
-
email:
|
|
119
117
|
executables: []
|
|
120
118
|
extensions: []
|
|
121
119
|
extra_rdoc_files:
|
|
@@ -150,7 +148,6 @@ homepage: https://github.com/svenfuchs/safemode
|
|
|
150
148
|
licenses:
|
|
151
149
|
- MIT
|
|
152
150
|
metadata: {}
|
|
153
|
-
post_install_message:
|
|
154
151
|
rdoc_options: []
|
|
155
152
|
require_paths:
|
|
156
153
|
- lib
|
|
@@ -158,19 +155,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
158
155
|
requirements:
|
|
159
156
|
- - ">="
|
|
160
157
|
- !ruby/object:Gem::Version
|
|
161
|
-
version: '
|
|
158
|
+
version: '3.0'
|
|
162
159
|
- - "<"
|
|
163
160
|
- !ruby/object:Gem::Version
|
|
164
|
-
version: '3.
|
|
161
|
+
version: '3.4'
|
|
165
162
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
163
|
requirements:
|
|
167
164
|
- - ">="
|
|
168
165
|
- !ruby/object:Gem::Version
|
|
169
166
|
version: '0'
|
|
170
167
|
requirements: []
|
|
171
|
-
rubygems_version:
|
|
172
|
-
signing_key:
|
|
168
|
+
rubygems_version: 4.0.10
|
|
173
169
|
specification_version: 4
|
|
174
|
-
summary: A library for safe evaluation of Ruby code based on
|
|
175
|
-
and Ruby2Ruby
|
|
170
|
+
summary: A library for safe evaluation of Ruby code based on Prism and Ruby2Ruby
|
|
176
171
|
test_files: []
|