class Safemode::Parser

Public Class Methods

jail(code, allowed_fcalls = []) click to toggle source
# File lib/safemode/parser.rb, line 7
def jail(code, allowed_fcalls = [])
  @@allowed_fcalls = allowed_fcalls
  tree = parse code
  self.new.process(tree)
end
parse(code) click to toggle source
# File lib/safemode/parser.rb, line 13
def parse(code)
  case @@parser
  # when 'ParseTree'
  #   ParseTree.translate(code)
  when 'RubyParser'
    RubyParser.new.parse(code)
  else
    raise "unknown parser #{@@parser}"
  end
end
parser=(parser) click to toggle source
# File lib/safemode/parser.rb, line 24
def parser=(parser)
  @@parser = parser
end

Public Instance Methods

jail(str, parentheses = false, safe_call: false) click to toggle source
# File lib/safemode/parser.rb, line 29
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) click to toggle source

split up process_call. see below …

# File lib/safemode/parser.rb, line 38
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) click to toggle source
# File lib/safemode/parser.rb, line 153
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) click to toggle source
# File lib/safemode/parser.rb, line 177
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) click to toggle source

split up Ruby2Ruby#process_call monster method so we can hook into it in a more readable manner

# File lib/safemode/parser.rb, line 146
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) click to toggle source
Calls superclass method
# File lib/safemode/parser.rb, line 125
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) click to toggle source
# File lib/safemode/parser.rb, line 47
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) click to toggle source
Calls superclass method
# File lib/safemode/parser.rb, line 67
def process_iasgn(exp)
  code = super
  if code != '@output_buffer = ""'
    raise_security_error(:iasgn, code)
  else
    code
  end
end
process_if(exp) click to toggle source

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 217
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
process_vcall(exp) click to toggle source
# File lib/safemode/parser.rb, line 57
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) click to toggle source
# File lib/safemode/parser.rb, line 139
def raise_security_error(type, info)
  raise Safemode::SecurityError.new(type, info)
end