Class Rufus::TreeChecker

  1. lib/rufus/treechecker.rb
Parent: Object

TreeChecker relies on ruby_parser to turns a piece of ruby code (a string) into a bunch of sexpression and then TreeChecker will check that sexpression tree and raise a Rufus::SecurityException if an excluded pattern is spotted.

The TreeChecker is meant to be useful for people writing DSLs directly in Ruby (not via their own parser) that want to check and prevent bad things from happening in this code.

tc = Rufus::TreeChecker.new do
  exclude_fvcall :abort
  exclude_fvcall :exit, :exit!
end

tc.check("1 + 1; abort")               # will raise a SecurityError
tc.check("puts (1..10).to_a.inspect")  # OK

featured exclusion methods

call / vcall / fcall ?

What the difference between those ? Well, here is how those various piece of code look like :

"exit"          => [:vcall, :exit]
"Kernel.exit"   => [:call, [:const, :Kernel], :exit]
"Kernel::exit"  => [:call, [:const, :Kernel], :exit]
"k.exit"        => [:call, [:vcall, :k], :exit]
"exit -1"       => [:fcall, :exit, [:array, [:lit, -1]]]

Obviously :fcall could be labelled as “function call”, :call is a call on to some instance, while vcall might either be a variable dereference or a function call with no arguments.

low-level rules

higher level rules

Those rules take no arguments

a bit further

It’s possible to clone a TreeChecker and to add some more rules to it :

tc0 = Rufus::TreeChecker.new do
  #
  # calls to eval, module_eval and instance_eval are not allowed
  #
  exclude_eval
end

tc1 = tc0.clone
tc1.add_rules do
  #
  # calls to any method on File and FileUtils classes are not allowed
  #
  exclude_call_on File, FileUtils
end

Classes and Modules

Class Rufus::TreeChecker::RuleSet

Constants

VERSION = '1.0.3'

Public class methods

new (&block)

initializes the TreeChecker, expects a block

[show source]
     # File lib/rufus/treechecker.rb, line 148
148:     def initialize (&block)
149: 
150:       @root_set = RuleSet.new
151:       @set = RuleSet.new
152:       @current_set = @set
153: 
154:       add_rules(&block)
155:     end

Public instance methods

add_rules (&block)

adds a set of checks (rules) to this treechecker. Returns self.

[show source]
     # File lib/rufus/treechecker.rb, line 196
196:     def add_rules (&block)
197: 
198:       instance_eval(&block) if block
199: 
200:       self
201:     end
check (rubycode)

Performs the check on the given String of ruby code. Will raise a Rufus::SecurityError if there is something excluded by the rules specified at the initialization of the TreeChecker instance.

[show source]
     # File lib/rufus/treechecker.rb, line 170
170:     def check (rubycode)
171: 
172:       sexp = parse(rubycode)
173: 
174:       #@root_checks.each do |meth, *args|
175:       #  send meth, sexp, args
176:       #end
177:       @root_set.check(sexp)
178: 
179:       do_check(sexp)
180:     end
clone ()

return a copy of this TreeChecker instance

[show source]
     # File lib/rufus/treechecker.rb, line 185
185:     def clone
186: 
187:       tc = TreeChecker.new
188:       tc.instance_variable_set(:@root_set, @root_set.clone)
189:       tc.instance_variable_set(:@set, @set.clone)
190:       tc
191:     end
freeze ()

freezes the treechecker instance “in depth“

[show source]
     # File lib/rufus/treechecker.rb, line 206
206:     def freeze
207:       super
208:       @root_set.freeze
209:       @set.freeze
210:     end
ptree (rubycode)

pretty-prints the sexp tree of the given rubycode

[show source]
     # File lib/rufus/treechecker.rb, line 133
133:     def ptree (rubycode)
134:       puts stree(rubycode)
135:     end
stree (rubycode)

returns the pretty-printed string of the given rubycode (thanks ruby_parser).

[show source]
     # File lib/rufus/treechecker.rb, line 141
141:     def stree (rubycode)
142:       "#{rubycode.inspect}\n =>\n#{parse(rubycode).inspect}"
143:     end
to_s ()
[show source]
     # File lib/rufus/treechecker.rb, line 157
157:     def to_s
158:       s = "#{self.class} (#{self.object_id})\n"
159:       s << "root_set :\n"
160:       s << @root_set.to_s
161:       s << "set :\n"
162:       s << @set.to_s
163:     end

Protected instance methods

at_root (&block)

within the ‘at_root’ block, rules are added to the @root_checks, ie they are evaluated only for the toplevel (root) sexp.

[show source]
     # File lib/rufus/treechecker.rb, line 327
327:     def at_root (&block)
328: 
329:       @current_set = @root_set
330:       add_rules(&block)
331:       @current_set = @set
332:     end
do_check (sexp)

the actual check method, check() is rather a bootstrap one...

[show source]
     # File lib/rufus/treechecker.rb, line 524
524:     def do_check (sexp)
525: 
526:       @set.check(sexp)
527: 
528:       return unless sexp.is_a?(Array) # check over, seems fine...
529: 
530:       # check children
531: 
532:       sexp.each { |c| do_check c }
533:     end
do_exclude_pair (first, args)
[show source]
     # File lib/rufus/treechecker.rb, line 511
511:     def do_exclude_pair (first, args)
512: 
513:       args, message = extract_message(args)
514:       args.each do |a|
515:         expand_class(a).each do |c|
516:           @current_set.exclude_pattern([ first, c ], message)
517:         end
518:       end
519:     end
exclude_access_to (*args)

prevents access (calling methods and rebinding) to a class (or a list of classes

[show source]
     # File lib/rufus/treechecker.rb, line 421
421:     def exclude_access_to (*args)
422:       exclude_call_on *args
423:       exclude_rebinding *args
424:     end
exclude_alias ()

bans the usage of ‘alias’

[show source]
     # File lib/rufus/treechecker.rb, line 478
478:     def exclude_alias
479: 
480:       @current_set.exclude_symbol(:alias, "'alias' is forbidden")
481:       @current_set.exclude_symbol(:alias_method, "'alias_method' is forbidden")
482:     end
exclude_backquotes ()

bans the use of backquotes

[show source]
     # File lib/rufus/treechecker.rb, line 497
497:     def exclude_backquotes
498: 
499:       @current_set.exclude_symbol(:xstr, 'backquotes are forbidden')
500:     end
exclude_call_on (*args)
[show source]
     # File lib/rufus/treechecker.rb, line 384
384:     def exclude_call_on (*args)
385:       do_exclude_pair(:call, args)
386:     end
exclude_call_to (*args)
[show source]
     # File lib/rufus/treechecker.rb, line 388
388:     def exclude_call_to (*args)
389:       args, message = extract_message(args)
390:       args.each { |a| @current_set.exclude_pattern([ :call, :any, a], message) }
391:     end
exclude_class_tinkering (*args)

bans the defintion and the [re]openening of classes

a list of exceptions (classes) can be passed. Subclassing those exceptions is permitted.

exclude_class_tinkering :except => [ String, Array ]
[show source]
     # File lib/rufus/treechecker.rb, line 442
442:     def exclude_class_tinkering (*args)
443: 
444:       @current_set.exclude_pattern(
445:         [ :sclass ], 'opening the metaclass of an instance is forbidden')
446: 
447:       Array(args.last[:except]).each { |e|
448:         expand_class(e).each do |c|
449:           @current_set.accept_pattern([ :class, :any, c ])
450:         end
451:       } if args.last.is_a?(Hash)
452: 
453:       @current_set.exclude_pattern(
454:         [ :class ], 'defining a class is forbidden')
455:     end
exclude_def ()

bans method definitions

[show source]
     # File lib/rufus/treechecker.rb, line 429
429:     def exclude_def
430: 
431:       @current_set.exclude_symbol(:defn, 'method definitions are forbidden')
432:     end
exclude_eval ()

bans the use of ‘eval’, ‘module_eval’ and ‘instance_eval‘

[show source]
     # File lib/rufus/treechecker.rb, line 487
487:     def exclude_eval
488: 
489:       exclude_call_to(:eval, 'eval() is forbidden')
490:       exclude_call_to(:module_eval, 'module_eval() is forbidden')
491:       exclude_call_to(:instance_eval, 'instance_eval() is forbidden')
492:     end
exclude_fcall (*args)
[show source]
     # File lib/rufus/treechecker.rb, line 371
371:     def exclude_fcall (*args)
372:       do_exclude_pair(:fcall, args)
373:     end
exclude_fvcall (*args)
[show source]
     # File lib/rufus/treechecker.rb, line 379
379:     def exclude_fvcall (*args)
380:       do_exclude_pair(:fcall, args)
381:       do_exclude_pair(:vcall, args)
382:     end
exclude_fvccall (*args)
[show source]
     # File lib/rufus/treechecker.rb, line 393
393:     def exclude_fvccall (*args)
394:       exclude_fvcall(*args)
395:       exclude_call_to(*args)
396:     end
exclude_global_vars ()

bans referencing or setting the value of global variables

[show source]
     # File lib/rufus/treechecker.rb, line 469
469:     def exclude_global_vars
470: 
471:       @current_set.exclude_symbol(:gvar, 'global vars are forbidden')
472:       @current_set.exclude_symbol(:gasgn, 'global vars are forbidden')
473:     end
exclude_head (head, message=nil)

adds a rule that will forbid sexps that begin with the given head

tc = TreeChecker.new do
  exclude_head [ :block ]
end

tc.check('a = 2')         # ok
tc.check('a = 2; b = 5')  # will raise an error as it's a block
[show source]
     # File lib/rufus/treechecker.rb, line 361
361:     def exclude_head (head, message=nil)
362: 
363:       @current_set.exclude_pattern(head, message)
364:     end
exclude_module_tinkering ()

bans the definition or the opening of modules

[show source]
     # File lib/rufus/treechecker.rb, line 460
460:     def exclude_module_tinkering
461: 
462:       @current_set.exclude_symbol(
463:         :module, 'defining or opening a module is forbidden')
464:     end
exclude_raise ()

bans raise and throw

[show source]
     # File lib/rufus/treechecker.rb, line 505
505:     def exclude_raise
506: 
507:       exclude_fvccall(:raise, 'raise is forbidden')
508:       exclude_fvccall(:throw, 'throw is forbidden')
509:     end
exclude_rebinding (*args)

This rule :

exclude_rebinding Kernel

will raise a security error for those pieces of code :

k = Kernel
k = ::Kernel
[show source]
     # File lib/rufus/treechecker.rb, line 408
408:     def exclude_rebinding (*args)
409:       args, message = extract_message(args)
410:       args.each do |a|
411:         expand_class(a).each do |c|
412:           @current_set.exclude_pattern([ :lasgn, :any, c], message)
413:         end
414:       end
415:     end
exclude_symbol (*args)
[show source]
     # File lib/rufus/treechecker.rb, line 366
366:     def exclude_symbol (*args)
367:       args, message = extract_message(args)
368:       args.each { |a| @current_set.exclude_symbol(a, message) }
369:     end
exclude_vcall (*args)
[show source]
     # File lib/rufus/treechecker.rb, line 375
375:     def exclude_vcall (*args)
376:       do_exclude_pair(:vcall, args)
377:     end
expand_class (arg)
[show source]
     # File lib/rufus/treechecker.rb, line 342
342:     def expand_class (arg)
343: 
344:       if arg.is_a?(Class) or arg.is_a?(Module)
345:         [ parse(arg.to_s), parse("::#{arg.to_s}") ]
346:       else
347:         [ arg ]
348:       end
349:     end
extract_message (args)
[show source]
     # File lib/rufus/treechecker.rb, line 334
334:     def extract_message (args)
335: 
336:       message = nil
337:       args = args.dup
338:       message = args.pop if args.last.is_a?(String)
339:       [ args, message ]
340:     end
parse (rubycode)

a simple parse (relies on ruby_parser currently)

[show source]
     # File lib/rufus/treechecker.rb, line 538
538:     def parse (rubycode)
539: 
540:       #(@parser ||= RubyParser.new).parse(rubycode).to_a
541:         #
542:         # parser goes ballistic after a while, seems having a new parser
543:         # each is not heavy at all
544: 
545:       RubyParser.new.parse(rubycode).to_a
546:     end