Class: Safemode::Parser

Inherits:
Ruby2Ruby
  • Object
show all
Defined in:
lib/safemode/parser.rb

Class Method Summary collapse

Instance Method Summary collapse

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
  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_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