public
Description: A library for safe evaluation of Ruby code based on ParseTree/RubyParser and Ruby2Ruby. Provides Rails ActionView template handlers for ERB and Haml.
Homepage: http://www.artweb-design.de
Clone URL: git://github.com/svenfuchs/safemode.git
safemode / lib / safemode / parser.rb
100644 185 lines (162 sloc) 6.314 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
module Safemode
  class Parser < Ruby2Ruby
    @@parser = defined?(RubyParser) ? 'RubyParser' : 'ParseTree'
    
    class << self
      def jail(code, allowed_fcalls = [])
        @@allowed_fcalls = allowed_fcalls
        tree = parse code
        self.new.process(tree)
      end
      
      def parse(code)
        case @@parser
        when 'ParseTree'
          ParseTree.translate(code)
        when 'RubyParser'
          RubyParser.new.parse(code)
        else
          raise "unknown parser #{@@parser}"
        end
      end
      
      def parser=(parser)
        @@parser = parser
      end
    end
    
    def jail(str, parentheses = false)
      str = parentheses ? "(#{str})." : "#{str}." if str
      "#{str}to_jail"
    end
 
    # split up #process_call. see below ...
    def process_call(exp)
      receiver = jail process_call_receiver(exp)
      name = exp.shift
      args = process_call_args(exp)
      process_call_code(receiver, name, args)
    end
    
    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
 
    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
    
    # see http://www.namikilab.tuat.ac.jp/~sasada/prog/rubynodes/nodes.html
    
    allowed = [ :call, :vcall, :evstr,
                   :lvar, :dvar, :ivar, :lasgn, :masgn, :dasgn, :dasgn_curr,
                   :lit, :str, :dstr, :dsym, :nil, :true, :false,
                   :array, :zarray, :hash, :dot2, :dot3, :flip2, :flip3,
                   :if, :case, :when, :while, :until, :iter, :for, :break, :next, :yield,
                   :and, :or, :not,
                   # not sure about self ...
                   :self,
                   # unnecessarily advanced?
                   :argscat, :argspush, :splat, :block_pass,
                   :op_asgn1, :op_asgn2, :op_asgn_and, :op_asgn_or,
                   # needed for haml
                   :block ]
            
    disallowed = [ # :self, # self doesn't seem to be needed for vcalls?
                   :const, :defn, :defs, :alias, :valias, :undef, :class, :attrset,
                   :module, :sclass, :colon2, :colon3,
                   :fbody, :scope, :args, :block_arg, :postexe,
                   :redo, :retry, :begin, :rescue, :resbody, :ensure,
                   :defined, :super, :zsuper, :return,
                   :dmethod, :bmethod, :to_ary, :svalue, :match,
                   :iasgn, :attrasgn, :cdecl, :cvasgn, :cvdecl, :cvar, :gvar, :gasgn,
                   :xstr, :dxstr,
                   # not sure how secure ruby regexp is, so leave it out for now
                   :dregx, :dregx_once, :match2, :match3, :nth_ref, :back_ref ]
 
    # SexpProcessor bails when we overwrite these ... but they are listed as
    # "internal nodes that you can't get to" in sexp_processor.rb
    # :ifunc, :method, :last, :opt_n, :cfunc, :newline, :alloca, :memo, :cref
                   
    disallowed.each do |name|
      define_method "process_#{name}" do
        code = super
        raise_security_error(name, code)
      end
    end
    
    def raise_security_error(type, info)
      raise Safemode::SecurityError.new(type, info)
    end
    
    # split up Ruby2Ruby#process_call monster method so we can hook into it
    # in a more readable manner
 
    def process_call_receiver(exp)
      receiver_node_type = exp.first.nil? ? nil : exp.first.first
      receiver = process exp.shift
      receiver = "(#{receiver})" if
        Ruby2Ruby::ASSIGN_NODES.include? receiver_node_type
      receiver
    end
 
    def process_call_args(exp)
      args_exp = exp.shift rescue nil
      if args_exp && args_exp.first == :array # FIX
        args = "#{process(args_exp)[1..-2]}"
      else
        args = process args_exp
        args = nil if args.empty?
      end
      args
    end
 
    def process_call_code(receiver, name, args)
      case name
      when :<=>, :==, :<, :>, :<=, :>=, :-, :+, :*, :/, :%, :<<, :>>, :** then
        "(#{receiver} #{name} #{args})"
      when :[] then
        "#{receiver}[#{args}]"
      when :"-@" then
        "-#{receiver}"
      when :"+@" then
        "+#{receiver}"
      else
        unless receiver.nil? then
          "#{receiver}.#{name}#{args ? "(#{args})" : args}"
        else
          "#{name}#{args ? "(#{args})" : args}"
        end
      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).
 
    def process_if(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
  end
end