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
- exclude_symbol : bans the usage of a
given symbol (very low-level,
mostly used by other rules
- exclude_head
- exclude_fcall
- exclude_vcall
- exclude_fvcall
- exclude_fvccall
- exclude_call_on
- exclude_call_to
- exclude_rebinding
- exclude_def
- exclude_class_tinkering
- exclude_module_tinkering
- at_root
higher level rules
Those rules take no arguments
- exclude_access_to : prevents calling or rebinding a list of classes
- exclude_eval : bans eval, module_eval and instance_eval
- exclude_global_vars : bans calling or modifying global vars
- exclude_alias : bans calls to alias and alias_method
- exclude_vm_exiting : bans exit, abort, …
- exclude_raise : bans calls to raise or throw
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
Methods
public class
public instance
protected instance
- at_root
- do_check
- do_exclude_pair
- exclude_access_to
- exclude_alias
- exclude_backquotes
- exclude_call_on
- exclude_call_to
- exclude_class_tinkering
- exclude_def
- exclude_eval
- exclude_fcall
- exclude_fvcall
- exclude_fvccall
- exclude_global_vars
- exclude_head
- exclude_module_tinkering
- exclude_raise
- exclude_rebinding
- exclude_symbol
- exclude_vcall
- expand_class
- extract_message
- parse
Constants
| VERSION | = | '1.0.3' |
Public class methods
initializes the TreeChecker, expects a block
# 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
adds a set of checks (rules) to this treechecker. Returns self.
# File lib/rufus/treechecker.rb, line 196 196: def add_rules (&block) 197: 198: instance_eval(&block) if block 199: 200: self 201: end
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.
# 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
return a copy of this TreeChecker instance
# 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
freezes the treechecker instance “in depth“
# File lib/rufus/treechecker.rb, line 206 206: def freeze 207: super 208: @root_set.freeze 209: @set.freeze 210: end
pretty-prints the sexp tree of the given rubycode
# File lib/rufus/treechecker.rb, line 133 133: def ptree (rubycode) 134: puts stree(rubycode) 135: end
returns the pretty-printed string of the given rubycode (thanks ruby_parser).
# File lib/rufus/treechecker.rb, line 141 141: def stree (rubycode) 142: "#{rubycode.inspect}\n =>\n#{parse(rubycode).inspect}" 143: end
# 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
within the ‘at_root’ block, rules are added to the @root_checks, ie they are evaluated only for the toplevel (root) sexp.
# 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
the actual check method, check() is rather a bootstrap one...
# 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
# 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
prevents access (calling methods and rebinding) to a class (or a list of classes
# File lib/rufus/treechecker.rb, line 421 421: def exclude_access_to (*args) 422: exclude_call_on *args 423: exclude_rebinding *args 424: end
bans the usage of ‘alias’
# 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
bans the use of backquotes
# File lib/rufus/treechecker.rb, line 497 497: def exclude_backquotes 498: 499: @current_set.exclude_symbol(:xstr, 'backquotes are forbidden') 500: end
# File lib/rufus/treechecker.rb, line 384 384: def exclude_call_on (*args) 385: do_exclude_pair(:call, args) 386: end
# 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
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 ]
# 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
bans method definitions
# File lib/rufus/treechecker.rb, line 429 429: def exclude_def 430: 431: @current_set.exclude_symbol(:defn, 'method definitions are forbidden') 432: end
bans the use of ‘eval’, ‘module_eval’ and ‘instance_eval‘
# 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
# File lib/rufus/treechecker.rb, line 371 371: def exclude_fcall (*args) 372: do_exclude_pair(:fcall, args) 373: end
# 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
# File lib/rufus/treechecker.rb, line 393 393: def exclude_fvccall (*args) 394: exclude_fvcall(*args) 395: exclude_call_to(*args) 396: end
bans referencing or setting the value of global variables
# 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
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
# File lib/rufus/treechecker.rb, line 361 361: def exclude_head (head, message=nil) 362: 363: @current_set.exclude_pattern(head, message) 364: end
bans the definition or the opening of modules
# 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
bans raise and throw
# 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
This rule :
exclude_rebinding Kernel
will raise a security error for those pieces of code :
k = Kernel k = ::Kernel
# 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
# 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
# File lib/rufus/treechecker.rb, line 375 375: def exclude_vcall (*args) 376: do_exclude_pair(:vcall, args) 377: end
# 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
# 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
a simple parse (relies on ruby_parser currently)
# 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