class Safemode::Parser
Public Class Methods
# File lib/safemode/parser.rb, line 4 def jail(code, allowed_fcalls = []) @@allowed_fcalls = allowed_fcalls tree = parse code self.new.process(tree) end
# File lib/safemode/parser.rb, line 10 def parse(code) Prism::Translation::RubyParser.parse(code) end
Public Instance Methods
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.
# 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
# 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
split up process_call. see below …
# 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
# 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
# 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
split up Ruby2Ruby#process_call monster method so we can hook into it in a more readable manner
# 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
# 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
# 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
# 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
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).
# File lib/safemode/parser.rb, line 254 def process_if(exp) exp.shift # remove ":if" symbol from exp expand = 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
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.
# 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
# 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
# File lib/safemode/parser.rb, line 133 def raise_security_error(type, info) raise Safemode::SecurityError.new(type, info) end