Class: Safemode::Parser
Class Method Summary collapse
Instance Method Summary collapse
-
#extract_guard(pattern) ⇒ Object
Prism translates guard clauses (in pattern if cond / in pattern unless cond) as s(:if, guard, pattern, nil) / s(:if, guard, nil, pattern).
- #jail(str, parentheses = false, safe_call: false) ⇒ Object
-
#process_call(exp, safe_call = false) ⇒ Object
split up #process_call.
- #process_call_args(name, args) ⇒ Object
- #process_call_code(receiver, name, args, safe_call) ⇒ Object
-
#process_call_receiver(recv) ⇒ Object
split up Ruby2Ruby#process_call monster method so we can hook into it in a more readable manner.
- #process_const(arg) ⇒ Object
- #process_fcall(exp) ⇒ Object
- #process_iasgn(exp) ⇒ Object
-
#process_if(exp) ⇒ Object
Ruby2Ruby process_if rewrites if and unless statements in a way that makes the result unusable for evaluation in, e.g.
-
#process_in(exp) ⇒ Object
Ruby2Ruby bug: __var adds ^ (pin) to all lvars inside :in context, including the body after “then” where ^ is invalid syntax.
- #process_vcall(exp) ⇒ Object
- #raise_security_error(type, info) ⇒ Object
Class Method Details
.jail(code, allowed_fcalls = []) ⇒ Object
4 5 6 7 8 |
# File 'lib/safemode/parser.rb', line 4 def jail(code, allowed_fcalls = []) @@allowed_fcalls = allowed_fcalls tree = parse code self.new.process(tree) end |
.parse(code) ⇒ Object
10 11 12 |
# File 'lib/safemode/parser.rb', line 10 def parse(code) Prism::Translation::RubyParser.parse(code) end |
Instance Method Details
#extract_guard(pattern) ⇒ Object
Prism translates guard clauses (in pattern if cond / in pattern unless cond) as s(:if, guard, pattern, nil) / s(:if, guard, nil, pattern). We detect this and rewrite the sexp in-place so process_if renders only the pattern, then return the guard string for process_in to append.
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/safemode/parser.rb', line 230 def extract_guard(pattern) return unless pattern.sexp_type == :if _, guard_sexp, if_body, else_body = pattern if if_body && !else_body guard_str = in_context(:in_body) { "if #{process guard_sexp.deep_clone}" } pattern.clear if_body.each { |node| pattern << node } elsif else_body && !if_body guard_str = in_context(:in_body) { "unless #{process guard_sexp.deep_clone}" } pattern.clear else_body.each { |node| pattern << node } end guard_str end |
#jail(str, parentheses = false, safe_call: false) ⇒ Object
15 16 17 18 19 20 21 |
# File 'lib/safemode/parser.rb', line 15 def jail(str, parentheses = false, safe_call: false) str = if str dot = safe_call ? "&." : "." parentheses ? "(#{str})#{dot}" : "#{str}#{dot}" end "#{str}to_jail" end |
#process_call(exp, safe_call = false) ⇒ Object
split up #process_call. see below …
24 25 26 27 28 29 30 31 |
# File 'lib/safemode/parser.rb', line 24 def process_call(exp, safe_call = false) _, recv, name, *args = exp receiver = jail(process_call_receiver(recv), safe_call: safe_call) arguments = process_call_args(name, args) process_call_code(receiver, name, arguments, safe_call) end |
#process_call_args(name, args) ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/safemode/parser.rb', line 147 def process_call_args(name, args) in_context :arglist do max = args.size - 1 args = args.map.with_index { |arg, i| arg_type = arg.sexp_type is_empty_hash = arg == s(:hash) arg = process arg next if arg.empty? strip_hash = (arg_type == :hash and not BINARY.include? name and not is_empty_hash and (i == max or args[i + 1].sexp_type == :splat)) wrap_arg = Ruby2Ruby::ASSIGN_NODES.include? arg_type arg = arg[2..-3] if strip_hash arg = "(#{arg})" if wrap_arg arg }.compact end end |
#process_call_code(receiver, name, args, safe_call) ⇒ Object
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/safemode/parser.rb', line 171 def process_call_code(receiver, name, args, safe_call) case name when *BINARY then if safe_call "#{receiver}&.#{name}(#{args.join(", ")})" elsif args.length > 1 "#{receiver}.#{name}(#{args.join(", ")})" else "(#{receiver} #{name} #{args.join(", ")})" end when :[] then receiver ||= "self" "#{receiver}[#{args.join(", ")}]" when :[]= then receiver ||= "self" rhs = args.pop "#{receiver}[#{args.join(", ")}] = #{rhs}" when :"!" then "(not #{receiver})" when :"-@" then "-#{receiver}" when :"+@" then "+#{receiver}" else args = nil if args.empty? args = "(#{args.join(", ")})" if args receiver = "#{receiver}." if receiver and not safe_call receiver = "#{receiver}&." if receiver and safe_call "#{receiver}#{name}#{args}" end end |
#process_call_receiver(recv) ⇒ Object
split up Ruby2Ruby#process_call monster method so we can hook into it in a more readable manner
140 141 142 143 144 145 |
# File 'lib/safemode/parser.rb', line 140 def process_call_receiver(recv) receiver_node_type = recv && recv.sexp_type receiver = process recv receiver = "(#{receiver})" if ASSIGN_NODES.include? receiver_node_type receiver end |
#process_const(arg) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/safemode/parser.rb', line 119 def process_const(arg) sexp_type = arg.sexp_body.sexp_type # constants are encoded as: "s(:const, :Encoding)" if sexp_type == :Encoding # handling of Encoding constants. # Note: ruby_parser evaluates __ENCODING__ to s(:colon2, s(:const, :Encoding), :UTF_8) "#{super(arg).gsub('-', '_')}" elsif sexp_type == :String # Allow String.new as used in ERB in Ruby 2.4+ to create a string buffer super(arg).to_s else raise_security_error("constant", super(arg)) end end |
#process_fcall(exp) ⇒ Object
33 34 35 36 37 38 39 40 41 |
# File 'lib/safemode/parser.rb', line 33 def process_fcall(exp) # using haml we probably never arrive here because :lasgn'ed :fcalls # somehow seem to change to :calls somewhere during processing # unless @@allowed_fcalls.include?(exp.first) # code = Ruby2Ruby.new.process([:fcall, exp[1], exp[2]]) # wtf ... # raise_security_error(exp.first, code) # end "to_jail.#{super}" end |
#process_iasgn(exp) ⇒ Object
53 54 55 56 57 58 59 60 |
# File 'lib/safemode/parser.rb', line 53 def process_iasgn(exp) code = super if code != '@output_buffer = ""' raise_security_error(:iasgn, code) else code end end |
#process_if(exp) ⇒ Object
Ruby2Ruby process_if rewrites if and unless statements in a way that makes the result unusable for evaluation in, e.g. ERB which appends a call to to_s when using <%= %> tags. We’d need to either enclose the result from process_if into parentheses like (1 if true) and (true ? (1) : (2)) or just use the plain if-then-else-end syntax (so that ERB can safely append to_s to the resulting block).
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/safemode/parser.rb', line 254 def process_if(exp) exp.shift # remove ":if" symbol from exp = Ruby2Ruby::ASSIGN_NODES.include? exp.first.first c = process exp.shift t = process exp.shift f = process exp.shift c = "(#{c.chomp})" if c =~ /\n/ if t then # unless expand then # if f then # r = "#{c} ? (#{t}) : (#{f})" # r = nil if r =~ /return/ # HACK - need contextual awareness or something # else # r = "#{t} if #{c}" # end # return r if r and (@indent+r).size < LINE_LENGTH and r !~ /\n/ # end r = "if #{c} then\n#{indent(t)}\n" r << "else\n#{indent(f)}\n" if f r << "end" r else # unless expand then # r = "#{f} unless #{c}" # return r if (@indent+r).size < LINE_LENGTH and r !~ /\n/ # end "unless #{c} then\n#{indent(f)}\nend" end end |
#process_in(exp) ⇒ Object
Ruby2Ruby bug: __var adds ^ (pin) to all lvars inside :in context, including the body after “then” where ^ is invalid syntax. Fix: process body outside the :in context.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/safemode/parser.rb', line 207 def process_in(exp) _, pattern, *body = exp guard = extract_guard(pattern) cond = process pattern body = body.compact.map { |sexp| in_context :in_body do indent process sexp end } body << indent("# do nothing") if body.empty? body = body.join "\n" header = "in #{cond}" header << " #{guard}" if guard "#{header} then\n#{body.chomp}" end |
#process_vcall(exp) ⇒ Object
43 44 45 46 47 48 49 50 51 |
# File 'lib/safemode/parser.rb', line 43 def process_vcall(exp) # unless @@allowed_fcalls.include?(exp.first) # code = Ruby2Ruby.new.process([:fcall, exp[1], exp[2]]) # wtf ... # raise_security_error(exp.first, code) # end name = exp[1] exp.clear "to_jail.#{name}" end |
#raise_security_error(type, info) ⇒ Object
133 134 135 |
# File 'lib/safemode/parser.rb', line 133 def raise_security_error(type, info) raise Safemode::SecurityError.new(type, info) end |