Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial release

  • Loading branch information...
commit 2d73032cd133fc6d29720f870c2db5674697989d 0 parents
Justin authored
Showing with 6,208 additions and 0 deletions.
  1. +16 −0 FEATURES
  2. +21 −0 LICENSE
  3. +112 −0 README.md
  4. +69 −0 WARNING_TYPES
  5. +266 −0 bin/brakeman
  6. +16 −0 brakeman.gemspec
  7. +67 −0 lib/checks.rb
  8. +338 −0 lib/checks/base_check.rb
  9. +216 −0 lib/checks/check_cross_site_scripting.rb
  10. +29 −0 lib/checks/check_default_routes.rb
  11. +29 −0 lib/checks/check_evaluation.rb
  12. +110 −0 lib/checks/check_execute.rb
  13. +46 −0 lib/checks/check_file_access.rb
  14. +25 −0 lib/checks/check_forgery_setting.rb
  15. +72 −0 lib/checks/check_mass_assignment.rb
  16. +36 −0 lib/checks/check_model_attributes.rb
  17. +98 −0 lib/checks/check_redirect.rb
  18. +65 −0 lib/checks/check_render.rb
  19. +64 −0 lib/checks/check_send.rb
  20. +15 −0 lib/checks/check_send_file.rb
  21. +36 −0 lib/checks/check_session_settings.rb
  22. +124 −0 lib/checks/check_sql.rb
  23. +54 −0 lib/checks/check_symbol_creation.rb
  24. +60 −0 lib/checks/check_validation_regex.rb
  25. +105 −0 lib/format/style.css
  26. +83 −0 lib/processor.rb
  27. +384 −0 lib/processors/alias_processor.rb
  28. +235 −0 lib/processors/base_processor.rb
  29. +146 −0 lib/processors/config_processor.rb
  30. +222 −0 lib/processors/controller_alias_processor.rb
  31. +175 −0 lib/processors/controller_processor.rb
  32. +84 −0 lib/processors/erb_template_processor.rb
  33. +62 −0 lib/processors/erubis_template_processor.rb
  34. +115 −0 lib/processors/haml_template_processor.rb
  35. +176 −0 lib/processors/lib/find_call.rb
  36. +39 −0 lib/processors/lib/find_model_call.rb
  37. +36 −0 lib/processors/lib/processor_helper.rb
  38. +118 −0 lib/processors/lib/render_helper.rb
  39. +117 −0 lib/processors/library_processor.rb
  40. +125 −0 lib/processors/model_processor.rb
  41. +204 −0 lib/processors/output_processor.rb
  42. +77 −0 lib/processors/params_processor.rb
  43. +338 −0 lib/processors/route_processor.rb
  44. +86 −0 lib/processors/template_alias_processor.rb
  45. +55 −0 lib/processors/template_processor.rb
  46. +628 −0 lib/report.rb
  47. +232 −0 lib/scanner.rb
  48. +144 −0 lib/tracker.rb
  49. +141 −0 lib/util.rb
  50. +97 −0 lib/warning.rb
16 FEATURES
@@ -0,0 +1,16 @@
+Can detect:
+-Possibly unescaped model attributes or parameters in views (Cross Site Scripting)
+-Bad string interpolation in calls to Model.find, Model.last, Model.first, etc., as well as chained calls (SQL Injection)
+-String interpolation in find_by_sql (SQL Injection)
+-String interpolation or params in calls to system, exec, and syscall and `` (Command Injection)
+-Unrestricted mass assignments
+-Global restriction of mass assignment
+-Missing call to protect_from_forgery in ApplicationController (CSRF protection)
+-Default routes, per-controller and globally
+-Redirects based on params (probably too broad currently)
+-Validation regexes not using \A and \z
+-Calls to render with dynamic paths
+
+General capabilities:
+-Search for method calls based on target class and/or method name
+-Determine 'output' of templates using ERB, Erubis, or HAML. Can handle automatic escaping
21 LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2010, YELLOWPAGES.COM, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
112 README.md
@@ -0,0 +1,112 @@
+# Brakeman
+
+Brakeman is a static analysis tool which checks Ruby on Rails applications for security vulnerabilities.
+
+It targets Rails versions > 2.0 and < 3.0.
+
+# Installation
+
+ gem build brakeman.gemspec
+ gem install brakeman*.gem
+
+# Usage
+
+ brakeman path/to/rails/app/root
+
+# Options
+
+To specify an output file for the results:
+
+ brakeman -o output_file path/to/rails/app/root
+
+The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, and `csv`.
+
+To suppress informational warnings and just output the report:
+
+ brakeman -q path/to/rails/app/root
+
+To see all kinds of debugging information:
+
+ brakeman -d path/to/rails/app/root
+
+Specific checks can be skipped, if desired. The name needs to be the correct case. For example, to skip looking for default routes (`DefaultRoutes`):
+
+ brakeman -x DefaultRoutes path/to/rails/app/root
+
+Multiple checks should be separated by a comma:
+
+ brakeman -x DefaultRoutes,Redirect path/to/rails/app/root
+
+To do the opposite and only run a certain set of tests:
+
+ brakeman -t Find,ValidationRegex path/to/rails/app/root
+
+To indicate certain methods are "safe":
+
+ brakeman -s benign_method,totally_safe path/to/rails/app/root
+
+By default, brakeman will assume that unknown methods involving untrusted data are dangerous. For example, this would a warning:
+
+ <%= some_method(:option => params[:input]) %>
+
+To only raise warnings only when untrusted data is being directly used:
+
+ brakeman -r path/to/rails/app/root
+
+# Warning information
+
+See WARNING_TYPES for more information on the warnings reported by this tool.
+
+# Warning context
+
+The HTML output format provides an excerpt from the original application source where a warning was triggered. Due to the processing done while looking for vulnerabilities, the source may not resemble the reported warning and reported line numbers may be slightly off. However, the context still provides a quick look into the code which raised the warning.
+
+# Confidence levels
+
+Brakeman assigns a confidence level to each warning. This provides a rough estimate of how certain the tool is that a given warning is actually a problem. Naturally, these ratings should not be taken as absolute truth.
+
+There are three levels of confidence:
+
+ + High - Either this is a simple warning (boolean value) or user input is very likely being used in unsafe ways.
+ + Medium - This generally indicates an unsafe use of a variable, but the variable may or may not be user input.
+ + Weak - Typically means user input was indirectly used in a potentially unsafe manner.
+
+To only get warnings above a given confidence level:
+
+ brakeman -w3 /path/to/rails/app/root
+
+The `-w` switch takes a number from 1 to 3, with 1 being low (all warnings) and 3 being high (only high confidence warnings).
+
+# Configuration files
+
+Brakeman options can stored and read from YAML files. To simplify the process of writing a configuration file, the `-C` option will output the currently set options.
+
+Options passed in on the commandline have priority over configuration files.
+
+The default config locations are `./config.yaml`, `~/.brakeman/`, and `/etc/brakeman/config.yaml`
+
+The `-c` option can be used to specify a configuration file to use.
+
+# License
+
+The MIT License
+
+Copyright (c) 2010, YELLOWPAGES.COM, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
69 WARNING_TYPES
@@ -0,0 +1,69 @@
+This file describes the various warning types reported by this tool.
+
+# Cross Site Scripting
+
+Cross site scripting warnings are raised when a parameter or model attribute is output through a view without being escaped.
+
+See http://guides.rubyonrails.org/security.html#cross-site-scripting-xss for details.
+
+# SQL Injection
+
+String interpolation or concatenation has been detected in an SQL query. Use parameterized queries instead.
+
+See http://guides.rubyonrails.org/security.html#sql-injection for details.
+
+# Command Injection
+
+Request parameters or string interpolation has been detected in a `system` call. This can lead to someone executing arbitrary commands. Use the safe form of `system` instead, which will pass in arguments safely.
+
+See http://guides.rubyonrails.org/security.html#command-line-injection for details.
+
+# Mass Assignment
+
+Mass assignment is a method for initializing models. If the attributes which are set is not restricted, someone may set the attributes to any value they wish.
+
+Mass assignment can be disabled globally.
+
+Please see http://railspikes.com/2008/9/22/is-your-rails-application-safe-from-mass-assignment for more details.
+
+# Attribute Restriction
+
+This warning comes up if a model does not limit what attributes can be set through mass assignment.
+
+In particular, this check looks for `attr_accessible` inside model definitions. If it is not found, this warning will be issued.
+
+Note that disabling mass assignment globally will suppress these warnings.
+
+# Cross-Site Request Forgery
+
+No call to `protect_from_forgery` was found in `ApplicationController`. This method prevents CSRF.
+
+See http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf for details.
+
+# Redirect
+
+Redirects which rely on user-supplied values can be used to "spoof" websites or hide malicious links in otherwise harmless-looking URLs. They can also allow access to restricted areas of a site if the destination is not validated.
+
+This warning is shown when request parameters are used inside a call to `redirect_to`.
+
+See http://www.owasp.org/index.php/Top_10_2010-A10 for more information.
+
+# Default Routes
+
+The general default routes warning means there is a call to `map.connect ":controller/:action/:id"` in config/routes.rb. This allows any public method on any controller to be called as an action.
+
+If this warning is reported for a particular controller, it means there is a route to that controller containing `:action`.
+
+Default routes can be dangerous if methods are made public which are not intended to be used as URLs or actions.
+
+# Format Validation
+
+Calls to `validates_format_of ..., :with => //` which do not use `\A` and `\z` as anchors will cause this warning. Using `^` and `$` is not sufficient, as `$` will only match up to a new line. This allows an attacker to put whatever malicious input they would like after a new line character.
+
+See http://guides.rubyonrails.org/security.html#regular-expressions for details.
+
+# Dynamic Render Path
+
+When a call to `render` uses a dynamically generated path, template name, file name, or action, there is the possibility that a user can access templates that should be restricted. The issue may be worse if those templates execute code or modify the database.
+
+This warning is shown whenever the path to be rendered is not a static string or symbol.
266 bin/brakeman
@@ -0,0 +1,266 @@
+#!/usr/bin/env ruby
+require "optparse"
+require 'set'
+require 'yaml'
+
+Version = "0.0.1"
+
+trap("INT") do
+ $stderr.puts "\nInterrupted - exiting."
+ exit!
+end
+
+#Load scanner
+begin
+ require 'scanner'
+rescue LoadError
+ #Try to find lib directory locally
+ $: << "#{File.expand_path(File.dirname(__FILE__))}/../lib"
+
+ begin
+ require 'scanner'
+ rescue LoadError
+ abort "Cannot find lib/ directory."
+ end
+end
+
+#Parse command line options
+options = {}
+
+OptionParser.new do |opts|
+ opts.banner = "Usage: brakeman [options] rails/root/path"
+
+ opts.on "-p", "--path PATH", "Specify path to Rails application" do |path|
+ options[:app_path] = File.expand_path path
+ end
+
+ opts.on "-q", "--quiet", "Suppress informational messages" do
+ options[:quiet] = true
+ $VERBOSE = nil
+ end
+
+ opts.separator ""
+ opts.separator "Scanning options:"
+
+ opts.on "--ignore-model-output", "Consider model attributes XSS-safe" do
+ options[:ignore_model_output] = true
+ end
+
+ opts.on "-r", "--report-direct", "Only report direct use of untrusted data" do |option|
+ options[:check_arguments] = !option
+ end
+
+ opts.on "-s", "--safe-methods meth1,meth2,etc", Array, "Consider the specified methods safe" do |methods|
+ options[:safe_methods] ||= Set.new
+ options[:safe_methods].merge methods.map {|e| e.to_sym }
+ end
+
+ opts.on "-t", "--test Check1,Check2,etc", Array, "Only run the specified checks" do |checks|
+ checks.each_with_index do |s, index|
+ if s[0,5] != "Check"
+ checks[index] = "Check" << s
+ end
+ end
+
+ options[:run_checks] ||= Set.new
+ options[:run_checks].merge checks
+ end
+
+ opts.on "-x", "--except Check1,Check2,etc", Array, "Skip the specified checks" do |skip|
+ skip.each do |s|
+ if s[0,5] != "Check"
+ s = "Check" << s
+ end
+
+ options[:skip_checks] ||= Set.new
+ options[:skip_checks] << s
+ end
+ end
+
+ opts.separator ""
+ opts.separator "Output options:"
+
+ opts.on "-d", "--debug", "Lots of output" do
+ options[:debug] = true
+ end
+
+ opts.on "-f",
+ "--format TYPE",
+ [:pdf, :text, :html, :csv],
+ "Specify output format. Default is text" do |type|
+
+ type = "s" if type == :text
+ options[:output_format] = ("to_" << type.to_s).to_sym
+ end
+
+ opts.on "-l", "--[no]-combine-locations", "Combine warning locations (Default)" do |combine|
+ options[:combine_locations] = combine
+ end
+
+ opts.on "-m", "--routes", "Report controller information" do
+ options[:report_routes] = true
+ end
+
+ opts.on "--message-limit LENGTH", "Limit message length in HTML report" do |limit|
+ options[:message_limit] = limit.to_i
+ end
+
+ opts.on "-o", "--output FILE", "Specify file for output. Defaults to stdout" do |file|
+ options[:output_file] = file
+ end
+
+ opts.on "-w",
+ "--confidence-level LEVEL",
+ ["1", "2", "3"],
+ "Set minimal confidence level (1 - 3). Default: 1" do |level|
+
+ options[:min_confidence] = 3 - level.to_i
+ end
+
+ opts.separator ""
+ opts.separator "Configuration files:"
+
+ opts.on "-c", "--config-file FILE", "Use specified configuration file" do |file|
+ options[:config_file] = File.expand_path(file)
+ end
+
+ opts.on "-C", "--create-config [FILE]", "Output configuration file based on options" do |file|
+ if file
+ options[:create_config] = file
+ else
+ options[:create_config] = true
+ end
+ end
+
+ opts.separator ""
+
+ opts.on_tail "-h", "--help", "Display this message" do
+ puts opts
+ exit
+ end
+end.parse!(ARGV)
+
+#Load configuation file
+[File.expand_path(options[:config_file].to_s),
+ File.expand_path("./config.yaml"),
+ File.expand_path("~/.brakeman/config.yaml"),
+ File.expand_path("/etc/brakeman/config.yaml"),
+ "#{File.expand_path(File.dirname(__FILE__))}/../lib/config.yaml"].each do |f|
+
+ if File.exist? f and not File.directory? f
+ warn "[Notice] Using configuration in #{f}" unless options[:quiet]
+ OPTIONS = YAML.load_file f
+ OPTIONS.merge! options
+ OPTIONS.each do |k,v|
+ if v.is_a? Array
+ OPTIONS[k] = Set.new v
+ end
+ end
+ break
+ end
+end
+
+OPTIONS = options unless defined? OPTIONS
+
+#Set defaults just in case
+{ :skip_checks => Set.new,
+ :check_arguments => true,
+ :safe_methods => Set.new,
+ :min_confidence => 2,
+ :combine_locations => true,
+ :collapse_mass_assignment => true,
+ :ignore_redirect_to_model => true,
+ :ignore_model_output => false,
+ :message_limit => 100,
+ :html_style => "#{File.expand_path(File.dirname(__FILE__))}/../lib/format/style.css"
+}.each do |k,v|
+ OPTIONS[k] = v if OPTIONS[k].nil?
+end
+
+
+#Set output format
+if OPTIONS[:output_format]
+ case OPTIONS[:output_format]
+ when :html, :to_html
+ OPTIONS[:output_format] = :to_html
+ when :csv, :to_csv
+ OPTIONS[:output_format] = :to_csv
+ when :pdf, :to_pdf
+ OPTIONS[:output_format] = :to_pdf
+ else
+ OPTIONS[:output_format] = :to_s
+ end
+else
+ case OPTIONS[:output_file]
+ when /\.html$/i
+ OPTIONS[:output_format] = :to_html
+ when /\.csv$/i
+ OPTIONS[:output_format] = :to_csv
+ when /\.pdf$/i
+ OPTIONS[:output_format] = :to_pdf
+ else
+ OPTIONS[:output_format] = :to_s
+ end
+end
+
+#Output configuration if requested
+if OPTIONS[:create_config]
+
+ if OPTIONS[:create_config].is_a? String
+ file = OPTIONS[:create_config]
+ else
+ file = nil
+ end
+
+ OPTIONS.delete :create_config
+
+ OPTIONS.each do |k,v|
+ if v.is_a? Set
+ OPTIONS[k] = v.to_a
+ end
+ end
+
+ if file
+ File.open file, "w" do |f|
+ YAML.dump OPTIONS, f
+ end
+ puts "Output configuration to #{file}"
+ else
+ puts YAML.dump(OPTIONS)
+ end
+ exit
+end
+
+
+#Check application path
+unless OPTIONS[:app_path]
+ if ARGV[-1].nil?
+ OPTIONS[:app_path] = File.expand_path "."
+ else
+ OPTIONS[:app_path] = File.expand_path ARGV[-1]
+ end
+end
+
+app_path = OPTIONS[:app_path]
+
+abort("Please supply the path to a Rails application.") unless app_path and File.exist? app_path + "/app"
+
+warn "[Notice] Using Ruby #{RUBY_VERSION}. Please make sure this matches the one used to run your Rails application."
+
+#Start scanning
+scanner = Scanner.new app_path
+
+warn "Processing application in #{app_path}"
+tracker = scanner.process
+
+warn "Running checks..."
+tracker.run_checks
+
+warn "Generating report..."
+if OPTIONS[:output_file]
+ File.open OPTIONS[:output_file], "w" do |f|
+ f.puts tracker.report.send(OPTIONS[:output_format])
+ end
+else
+ puts tracker.report.send(OPTIONS[:output_format])
+end
16 brakeman.gemspec
@@ -0,0 +1,16 @@
+Gem::Specification.new do |s|
+ s.name = %q{brakeman}
+ s.version = "0.0.1"
+ s.authors = ["Justin Collins"]
+ s.email = "justin@presidentbeef.com"
+ s.summary = "Security vulnerability scanner for Ruby on Rails."
+ s.description = "Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis."
+ s.homepage = "http://github.com/presidentbeef/brakeman"
+ s.files = ["bin/brakeman", "WARNING_TYPES", "FEATURES", "README.md"] + Dir["lib/**/*.rb"] + Dir["lib/format/*.css"]
+ s.executables = ["brakeman"]
+ s.add_dependency "activesupport", "~>2.2.2"
+ s.add_dependency "ruby2ruby", "~>1.2.4"
+ s.add_dependency "ruport", "~>1.6.3"
+ s.add_dependency "erubis", "~>2.6.5"
+ s.add_dependency "haml", "~>3.0.12"
+end
67 lib/checks.rb
@@ -0,0 +1,67 @@
+#Collects up results from running different checks.
+#
+#Checks can be added with +Check.add(check_class)+
+#
+#All .rb files in checks/ will be loaded.
+class Checks
+ @checks = []
+
+ attr_reader :warnings, :controller_warnings, :model_warnings, :template_warnings, :checks_run
+
+ #Add a check. This will call +_klass_.new+ when running tests
+ def self.add klass
+ @checks << klass
+ end
+
+ #No need to use this directly.
+ def initialize
+ @warnings = []
+ @template_warnings = []
+ @model_warnings = []
+ @controller_warnings = []
+ @checks_run = []
+ end
+
+ #Add Warning to list of warnings to report.
+ #Warnings are split into four different arrays
+ #for template, controller, model, and generic warnings.
+ def add_warning warning
+ case warning.warning_set
+ when :template
+ @template_warnings << warning
+ when :warning
+ @warnings << warning
+ when :controller
+ @controller_warnings << warning
+ when :model
+ @model_warnings << warning
+ else
+ raise "Unknown warning: #{warning.warning_set}"
+ end
+ end
+
+ #Run all the checks on the given Tracker.
+ #Returns a new instance of Checks with the results.
+ def self.run_checks tracker
+ checks = self.new
+ @checks.each do |c|
+ #Run or don't run check based on options
+ unless OPTIONS[:skip_checks].include? c.to_s or
+ (OPTIONS[:run_checks] and not OPTIONS[:run_checks].include? c.to_s)
+
+ warn " - #{c}"
+ c.new(checks, tracker).run_check
+
+ #Maintain list of which checks were run
+ #mainly for reporting purposes
+ checks.checks_run << c.to_s[5..-1]
+ end
+ end
+ checks
+ end
+end
+
+#Load all files in checks/ directory
+Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/checks/*.rb").sort.each do |f|
+ require f.match(/(checks\/.*)\.rb$/)[0]
+end
338 lib/checks/base_check.rb
@@ -0,0 +1,338 @@
+require 'rubygems'
+require 'sexp_processor'
+require 'processors/output_processor'
+require 'warning'
+require 'util'
+
+#Basis of vulnerability checks.
+class BaseCheck < SexpProcessor
+ include ProcessorHelper
+ include Util
+ attr_reader :checks, :tracker
+
+ CONFIDENCE = { :high => 0, :med => 1, :low => 2 }
+
+ #Initialize Check with Checks.
+ def initialize checks, tracker
+ super()
+ @results = [] #only to check for duplicates
+ @checks = checks
+ @tracker = tracker
+ @string_interp = false
+ @current_template = @current_module = @current_class = @current_method = nil
+ self.strict = false
+ self.auto_shift_type = false
+ self.require_empty = false
+ self.default_method = :process_default
+ self.warn_on_default = false
+ end
+
+ #Add result to result list, which is used to check for duplicates
+ def add_result result, location = nil
+ location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
+ location = location[:name] if location.is_a? Hash
+ location = location.to_sym
+
+ @results << [result.line, location, result]
+ end
+
+ #Default Sexp processing. Iterates over each value in the Sexp
+ #and processes them if they are also Sexps.
+ def process_default exp
+ type = exp.shift
+ exp.each_with_index do |e, i|
+ if sexp? e
+ process e
+ else
+ e
+ end
+ end
+
+ exp.unshift type
+ end
+
+ #Process calls and check if they include user input
+ def process_call exp
+ process exp[1] if sexp? exp[1]
+ process exp[3]
+
+ if ALL_PARAMETERS.include? exp[1] or ALL_PARAMETERS.include? exp or params? exp[1]
+ @has_user_input = :params
+ elsif exp[1] == COOKIES or exp == COOKIES or cookies? exp[1]
+ @has_user_input = :cookies
+ elsif sexp? exp[1] and model_name? exp[1][1]
+ @has_user_input = :model
+ end
+
+ exp
+ end
+
+ #Note that params are included in current expression
+ def process_params exp
+ @has_user_input = :params
+ exp
+ end
+
+ #Note that cookies are included in current expression
+ def process_cookies exp
+ @has_user_input = :cookies
+ exp
+ end
+
+ private
+
+ #Report a warning
+ def warn options
+ @checks.add_warning Warning.new(options.merge({ :check => self.class.to_s }))
+ end
+
+ #Run _exp_ through OutputProcessor to get a nice String.
+ def format_output exp
+ OutputProcessor.new.format(exp).gsub(/\r|\n/, "")
+ end
+
+ #Checks if the model inherits from parent,
+ def parent? tracker, model, parent
+ if model == nil
+ false
+ elsif model[:parent] == parent
+ true
+ elsif model[:parent]
+ parent? tracker, tracker.models[model[:parent]], parent
+ else
+ false
+ end
+ end
+
+ #Checks if mass assignment is disabled globally in an initializer.
+ def mass_assign_disabled? tracker
+ matches = tracker.check_initializers(:"ActiveRecord::Base", :send)
+ if matches.empty?
+ false
+ else
+ matches.each do |result|
+ if result[3][3] == Sexp.new(:arg_list, Sexp.new(:lit, :attr_accessible), Sexp.new(:nil))
+ return true
+ end
+ end
+ end
+ end
+
+ #This is to avoid reporting duplicates. Checks if the result has been
+ #reported already from the same line number.
+ def duplicate? result, location = nil
+ line = result.line
+ location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
+
+ location = location[:name] if location.is_a? Hash
+ location = location.to_sym
+ @results.each do |r|
+ if r[0] == line and r[1] == location
+ if OPTIONS[:combine_locations]
+ return true
+ elsif r[2] == result
+ return true
+ end
+ end
+ end
+
+ false
+ end
+
+ #Ignores ignores
+ def process_ignore exp
+ exp
+ end
+
+ #Does not actually process string interpolation, but notes that it occurred.
+ def process_string_interp exp
+ @string_interp = true
+ exp
+ end
+
+ #Checks if an expression contains string interpolation.
+ def include_interp? exp
+ @string_interp = false
+ process exp
+ @string_interp
+ end
+
+ #Checks if _exp_ includes parameters or cookies, but this only works
+ #with the base process_default.
+ def include_user_input? exp
+ @has_user_input = false
+ process exp
+ @has_user_input
+ end
+
+ #This is used to check for user input being used directly.
+ #
+ #Returns false if none is found, otherwise it returns an array
+ #where the first element is the type of user input
+ #(either :params or :cookies) and the second element is the matching
+ #expression
+ def has_immediate_user_input? exp
+ if params? exp
+ return :params, exp
+ elsif cookies? exp
+ return :cookies, exp
+ elsif call? exp
+ if sexp? exp[1]
+ if ALL_PARAMETERS.include? exp[1] or params? exp[1]
+ return :params, exp
+ elsif exp[1] == COOKIES
+ return :cookies, exp
+ else
+ false
+ end
+ else
+ false
+ end
+ elsif sexp? exp
+ case exp.node_type
+ when :string_interp
+ exp.each do |e|
+ if sexp? e
+ type, match = has_immediate_user_input?(e)
+ if type
+ return type, match
+ end
+ end
+ end
+ false
+ when :string_eval
+ if sexp? exp[1]
+ if exp[1].node_type == :rlist
+ exp[1].each do |e|
+ if sexp? e
+ type, match = has_immediate_user_input?(e)
+ if type
+ return type, match
+ end
+ end
+ end
+ false
+ else
+ has_immediate_user_input? exp[1]
+ end
+ end
+ when :format
+ has_immediate_user_input? exp[1]
+ when :if
+ (sexp? exp[2] and has_immediate_user_input? exp[2]) or
+ (sexp? exp[3] and has_immediate_user_input? exp[3])
+ else
+ false
+ end
+ end
+ end
+
+ #Checks for a model attribute at the top level of the
+ #expression.
+ def has_immediate_model? exp, out = nil
+ out = exp if out.nil?
+
+ if sexp? exp and exp.node_type == :output
+ exp = exp[1]
+ end
+
+ if call? exp
+ target = exp[1]
+ method = exp[2]
+
+ if call? target and not method.to_s[-1,1] == "?"
+ has_immediate_model? target, out
+ elsif model_name? target
+ exp
+ else
+ false
+ end
+ elsif sexp? exp
+ case exp.node_type
+ when :string_interp
+ exp.each do |e|
+ if sexp? e and match = has_immediate_model?(e, out)
+ return match
+ end
+ end
+ false
+ when :string_eval
+ if sexp? exp[1]
+ if exp[1].node_type == :rlist
+ exp[1].each do |e|
+ if sexp? e and match = has_immediate_model?(e, out)
+ return match
+ end
+ end
+ false
+ else
+ has_immediate_model? exp[1], out
+ end
+ end
+ when :format
+ has_immediate_model? exp[1], out
+ when :if
+ ((sexp? exp[2] and has_immediate_model? exp[2], out) or
+ (sexp? exp[3] and has_immediate_model? exp[3], out))
+ else
+ false
+ end
+ end
+ end
+
+ #Checks if +exp+ is a model name.
+ #
+ #Prior to using this method, either @tracker must be set to
+ #the current tracker, or else @models should contain an array of the model
+ #names, which is available via tracker.models.keys
+ def model_name? exp
+ @models ||= @tracker.models.keys
+
+ if exp.is_a? Symbol
+ @models.include? exp
+ elsif sexp? exp
+ klass = nil
+ begin
+ klass = class_name exp
+ rescue StandardError
+ end
+
+ klass and @models.include? klass
+ else
+ false
+ end
+ end
+
+ #Finds entire method call chain where +target+ is a target in the chain
+ def find_chain exp, target
+ return unless sexp? exp
+
+ case exp.node_type
+ when :output, :format
+ find_chain exp[1], target
+ when :call
+ if exp == target or include_target? exp, target
+ return exp
+ end
+ else
+ exp.each do |e|
+ if sexp? e
+ res = find_chain e, target
+ return res if res
+ end
+ end
+ nil
+ end
+ end
+
+ #Returns true if +target+ is in +exp+
+ def include_target? exp, target
+ return false unless call? exp
+
+ exp.each do |e|
+ return true if e == target or include_target? e, target
+ end
+
+ false
+ end
+end
216 lib/checks/check_cross_site_scripting.rb
@@ -0,0 +1,216 @@
+require 'checks/base_check'
+require 'processors/lib/find_call'
+require 'processors/lib/processor_helper'
+require 'util'
+require 'set'
+
+#This check looks for unescaped output in templates which contains
+#parameters or model attributes.
+#
+#For example:
+#
+# <%= User.find(:id).name %>
+# <%= params[:id] %>
+class CheckCrossSiteScripting < BaseCheck
+ Checks.add self
+
+ #Ignore these methods and their arguments.
+ #It is assumed they will take care of escaping their output.
+ IGNORE_METHODS = Set.new([:h, :escapeHTML, :link_to, :text_field_tag, :hidden_field_tag,
+ :image_tag, :select, :submit_tag, :hidden_field, :url_encode,
+ :radio_button, :will_paginate, :button_to, :url_for, :mail_to,
+ :fields_for, :label, :text_area, :text_field, :hidden_field, :check_box,
+ :field_field])
+
+ IGNORE_MODEL_METHODS = Set.new([:average, :count, :maximum, :minimum, :sum])
+
+ MODEL_METHODS = Set.new([:all, :find, :first, :last, :new])
+
+ IGNORE_LIKE = /^link_to_|_path|_tag|_url$/
+
+ HAML_HELPERS = Sexp.new(:colon2, Sexp.new(:const, :Haml), :Helpers)
+
+ URI = Sexp.new(:const, :URI)
+
+ CGI = Sexp.new(:const, :CGI)
+
+ FORM_BUILDER = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new, Sexp.new(:arglist))
+
+ #Run check
+ def run_check
+ IGNORE_METHODS.merge OPTIONS[:safe_methods]
+ @models = tracker.models.keys
+ @inspect_arguments = OPTIONS[:check_arguments]
+
+ tracker.each_template do |name, template|
+ @current_template = template
+
+ template[:outputs].each do |out|
+ type, match = has_immediate_user_input?(out[1])
+ if type
+ unless duplicate? out
+ add_result out
+ case type
+ when :params
+
+ warn :template => @current_template,
+ :warning_type => "Cross Site Scripting",
+ :message => "Unescaped parameter value",
+ :line => match.line,
+ :code => match,
+ :confidence => CONFIDENCE[:high]
+
+ when :cookies
+
+ warn :template => @current_template,
+ :warning_type => "Cross Site Scripting",
+ :message => "Unescaped cookie value",
+ :line => match.line,
+ :code => match,
+ :confidence => CONFIDENCE[:high]
+ end
+ end
+ elsif not OPTIONS[:ignore_model_output] and match = has_immediate_model?(out[1])
+ method = match[2]
+
+ unless duplicate? out or IGNORE_MODEL_METHODS.include? method
+ add_result out
+
+ if MODEL_METHODS.include? method or method.to_s =~ /^find_by/
+ confidence = CONFIDENCE[:high]
+ else
+ confidence = CONFIDENCE[:med]
+ end
+
+ code = find_chain out, match
+ warn :template => @current_template,
+ :warning_type => "Cross Site Scripting",
+ :message => "Unescaped model attribute",
+ :line => code.line,
+ :code => code,
+ :confidence => confidence
+ end
+
+ else
+ @matched = false
+ @mark = false
+ process out
+ end
+ end
+ end
+ end
+
+ #Process an output Sexp
+ def process_output exp
+ process exp[1]
+ end
+
+ #Check a call for user input
+ #
+ #
+ #Since we want to report an entire call and not just part of one, use @mark
+ #to mark when a call is started. Any dangerous values inside will then
+ #report the entire call chain.
+ def process_call exp
+ if @mark
+ actually_process_call exp
+ else
+ @mark = true
+ actually_process_call exp
+ message = nil
+
+ if @matched == :model and not OPTIONS[:ignore_model_output]
+ message = "Unescaped model attribute"
+ elsif @matched == :params
+ message = "Unescaped parameter value"
+ end
+
+ if message and not duplicate? exp
+ add_result exp
+
+ warn :template => @current_template,
+ :warning_type => "Cross Site Scripting",
+ :message => message,
+ :line => exp.line,
+ :code => exp,
+ :confidence => CONFIDENCE[:low]
+ end
+
+ @mark = @matched = false
+ end
+
+ exp
+ end
+
+ def actually_process_call exp
+ return if @matched
+ target = exp[1]
+ if sexp? target
+ target = process target
+ end
+
+ method = exp[2]
+ args = exp[3]
+
+ #Ignore safe items
+ if (target.nil? and (IGNORE_METHODS.include? method or method.to_s =~ IGNORE_LIKE)) or
+ (@matched == :model and IGNORE_MODEL_METHODS.include? method) or
+ (target == HAML_HELPERS and method == :html_escape) or
+ ((target == URI or target == CGI) and method == :escape) or
+ (target == FORM_BUILDER and IGNORE_METHODS.include? method) or
+ (method.to_s[-1,1] == "?")
+
+ exp[0] = :ignore
+ @matched = false
+ elsif sexp? exp[1] and model_name? exp[1][1]
+
+ @matched = :model
+ elsif @inspect_arguments and (ALL_PARAMETERS.include?(exp) or params? exp)
+
+ @matched = :params
+ else
+ process args if @inspect_arguments
+ end
+ end
+
+ #Note that params have been found
+ def process_params exp
+ @matched = :params
+ exp
+ end
+
+ #Note that cookies have been found
+ def process_cookies exp
+ @matched = :cookies
+ exp
+ end
+
+ #Ignore calls to render
+ def process_render exp
+ exp
+ end
+
+ #Process as default
+ def process_string_interp exp
+ process_default exp
+ end
+
+ #Process as default
+ def process_format exp
+ process_default exp
+ end
+
+ #Ignore output HTML escaped via HAML
+ def process_format_escaped exp
+ exp
+ end
+
+ #Ignore condition in if Sexp
+ def process_if exp
+ exp[2..-1].each do |e|
+ process e if sexp? e
+ end
+ exp
+ end
+
+end
29 lib/checks/check_default_routes.rb
@@ -0,0 +1,29 @@
+require 'checks/base_check'
+
+#Checks if default routes are allowed in routes.rb
+class CheckDefaultRoutes < BaseCheck
+ Checks.add self
+
+ #Checks for :allow_all_actions globally and for individual routes
+ #if it is not enabled globally.
+ def run_check
+ if tracker.routes[:allow_all_actions]
+ #Default routes are enabled globally
+ warn :warning_type => "Default Routes",
+ :message => "All public methods in controllers are available as actions in routes.rb",
+ :line => tracker.routes[:allow_all_actions].line,
+ :confidence => CONFIDENCE[:high],
+ :file => "#{OPTIONS[:app_path]}/config/routes.rb"
+ else #Report each controller separately
+ tracker.routes.each do |name, actions|
+ if actions == :allow_all_actions
+ warn :controller => name,
+ :warning_type => "Default Routes",
+ :message => "Any public method in #{name} can be used as an action.",
+ :confidence => CONFIDENCE[:med],
+ :file => "#{OPTIONS[:app_path]}/config/routes.rb"
+ end
+ end
+ end
+ end
+end
29 lib/checks/check_evaluation.rb
@@ -0,0 +1,29 @@
+require 'checks/base_check'
+
+#This check looks for calls to +eval+, +instance_eval+, etc. which include
+#user input.
+class CheckEvaluation < BaseCheck
+ Checks.add self
+
+ #Process calls
+ def run_check
+ calls = tracker.find_call nil, [:eval, :instance_eval, :class_eval, :module_eval]
+
+ @templates = tracker.templates
+
+ calls.each do |call|
+ process_result call
+ end
+ end
+
+ #Warns if result includes user input
+ def process_result result
+ if include_user_input? result[-1]
+ warn :result => result,
+ :warning_type => "Dangerous Eval",
+ :message => "User input in eval",
+ :code => result[-1],
+ :confidence => CONFIDENCE[:high]
+ end
+ end
+end
110 lib/checks/check_execute.rb
@@ -0,0 +1,110 @@
+require 'checks/base_check'
+require 'processors/lib/find_call'
+
+#Checks for string interpolation and parameters in calls to
+#Kernel#system, Kernel#exec, Kernel#syscall, and inside backticks.
+#
+#Examples of command injection vulnerabilities:
+#
+# system("rf -rf #{params[:file]}")
+# exec(params[:command])
+# `unlink #{params[:something}`
+class CheckExecute < BaseCheck
+ Checks.add self
+
+ #Check models, controllers, and views for command injection.
+ def run_check
+ check_for_backticks tracker
+
+ calls = tracker.find_call [:IO, :Open3, :Kernel, []], [:exec, :popen, :popen3, :syscall, :system]
+
+ calls.each do |result|
+ process result
+ end
+ end
+
+ #Processes results from FindCall.
+ def process_result exp
+ call = exp[-1]
+
+ args = process call[3]
+
+ case call[2]
+ when :system, :exec
+ failure = include_user_input?(args[1]) || include_interp?(args[1])
+ else
+ failure = include_user_input?(args) || include_interp?(args)
+ end
+
+ if failure and not duplicate? call, exp[1]
+ add_result call, exp[1]
+
+ if @string_interp
+ confidence = CONFIDENCE[:med]
+ else
+ confidence = CONFIDENCE[:high]
+ end
+
+ warn :result => exp,
+ :warning_type => "Command Injection",
+ :message => "Possible command injection",
+ :line => call.line,
+ :code => call,
+ :confidence => confidence
+ end
+
+ exp
+ end
+
+ #Looks for calls using backticks such as
+ #
+ # `rm -rf #{params[:file]}`
+ def check_for_backticks tracker
+ tracker.each_method do |exp, set_name, method_name|
+ @current_set = set_name
+ @current_method = method_name
+
+ process exp
+ end
+
+ @current_set = nil
+
+ tracker.each_template do |name, template|
+ @current_template = template
+
+ process template[:src]
+ end
+
+ @current_template = nil
+ end
+
+ #Processes backticks.
+ def process_dxstr exp
+ return exp if duplicate? exp
+
+ add_result exp
+
+ if include_user_input? exp
+ confidence = CONFIDENCE[:high]
+ else
+ confidence = CONFIDENCE[:med]
+ end
+
+ warning = { :warning_type => "Command Injection",
+ :message => "Possible command injection",
+ :line => exp.line,
+ :code => exp,
+ :confidence => confidence }
+
+ if @current_template
+ warning[:template] = @current_template
+ else
+ warning[:class] = @current_set
+ warning[:method] = @current_method
+ end
+
+ warn warning
+
+ exp
+ end
+end
46 lib/checks/check_file_access.rb
@@ -0,0 +1,46 @@
+require 'checks/base_check'
+require 'processors/lib/processor_helper'
+
+#Checks for user input in methods which open or manipulate files
+class CheckFileAccess < BaseCheck
+ Checks.add self
+
+ def run_check
+ methods = tracker.find_call [[:Dir, :File, :IO, :Kernel, :"Net::FTP", :"Net::HTTP", :PStore, :Pathname, :Shell, :YAML], []], [:[], :chdir, :chroot, :delete, :entries, :foreach, :glob, :install, :lchmod, :lchown, :link, :load, :load_file, :makedirs, :move, :new, :open, :read, :read_lines, :rename, :rmdir, :safe_unlink, :symlink, :syscopy, :sysopen, :truncate, :unlink]
+
+ methods.concat tracker.find_call(:FileUtils, nil)
+
+ methods.each do |call|
+ process_result call
+ end
+ end
+
+ def process_result result
+ call = result[-1]
+
+ file_name = call[3][1]
+
+ if check = include_user_input?(file_name)
+ unless duplicate? call, result[1]
+ add_result call, result[1]
+
+ if check == :params
+ message = "Parameter"
+ elsif check == :cookies
+ message = "Cookie"
+ else
+ message = "User input"
+ end
+
+ message << " value used in file name"
+
+ warn :result => result,
+ :warning_type => "File Access",
+ :message => message,
+ :confidence => CONFIDENCE[:high],
+ :line => call.line,
+ :code => call
+ end
+ end
+ end
+end
25 lib/checks/check_forgery_setting.rb
@@ -0,0 +1,25 @@
+require 'checks/base_check'
+
+#Checks that +protect_from_forgery+ is set in the ApplicationController
+class CheckForgerySetting < BaseCheck
+ Checks.add self
+
+ def run_check
+ app_controller = tracker.controllers[:ApplicationController]
+ if tracker.config[:rails][:action_controller] and
+ tracker.config[:rails][:action_controller][:allow_forgery_protection] == Sexp.new(:false)
+
+ warn :controller => :ApplicationController,
+ :warning_type => "Cross Site Request Forgery",
+ :message => "Forgery protection is disabled",
+ :confidence => CONFIDENCE[:high]
+
+ elsif app_controller and not app_controller[:options][:protect_from_forgery]
+
+ warn :controller => :ApplicationController,
+ :warning_type => "Cross-Site Request Forgery",
+ :message => "'protect_from_forgery' should be called in ApplicationController",
+ :confidence => CONFIDENCE[:high]
+ end
+ end
+end
72 lib/checks/check_mass_assignment.rb
@@ -0,0 +1,72 @@
+require 'checks/base_check'
+
+#Checks for mass assignments to models.
+#
+#See http://guides.rubyonrails.org/security.html#mass-assignment for details
+class CheckMassAssignment < BaseCheck
+ Checks.add self
+
+ def run_check
+ return if mass_assign_disabled? tracker
+
+ models = []
+ tracker.models.each do |name, m|
+ if parent?(tracker, m, :"ActiveRecord::Base") and m[:attr_accessible].nil?
+ models << name
+ end
+ end
+
+ return if models.empty?
+
+ @results = Set.new
+
+ calls = tracker.find_call models, [:new,
+ :attributes=,
+ :update_attribute,
+ :update_attributes,
+ :update_attributes!]
+
+ calls.each do |result|
+ process result
+ end
+ end
+
+ #All results should be Model.new(...) or Model.attributes=() calls
+ def process_result res
+ call = res[-1]
+
+ check = check_call call
+
+ if check and not @results.include? call
+ @results << call
+
+ if include_user_input? call[3]
+ confidence = CONFIDENCE[:high]
+ else
+ confidence = CONFIDENCE[:med]
+ end
+
+ warn :result => res,
+ :warning_type => "Mass Assignment",
+ :message => "Unprotected mass assignment",
+ :line => call.line,
+ :code => call,
+ :confidence => confidence
+ end
+ res
+ end
+
+ #Want to ignore calls to Model.new that have no arguments
+ def check_call call
+ args = process call[3]
+ if args.length <= 1 #empty new()
+ false
+ elsif hash? args[1]
+ #Still should probably check contents of hash
+ false
+ else
+ true
+ end
+ end
+
+end
36 lib/checks/check_model_attributes.rb
@@ -0,0 +1,36 @@
+require 'checks/base_check'
+
+#Check if mass assignment is used with models
+#which inherit from ActiveRecord::Base.
+#
+#If OPTIONS[:collapse_mass_assignment] is +true+ (default), all models which do
+#not use attr_accessible will be reported in a single warning
+class CheckModelAttributes < BaseCheck
+ Checks.add self
+
+ def run_check
+ return if mass_assign_disabled? tracker
+
+ names = []
+
+ tracker.models.each do |name, model|
+ if model[:attr_accessible].nil? and parent? tracker, model, :"ActiveRecord::Base"
+ if OPTIONS[:collapse_mass_assignment]
+ names << name.to_s
+ else
+ warn :model => name,
+ :warning_type => "Attribute Restriction",
+ :message => "Mass assignment is not restricted using attr_accessible",
+ :confidence => CONFIDENCE[:high]
+ end
+ end
+ end
+
+ if OPTIONS[:collapse_mass_assignment] and not names.empty?
+ warn :model => names.sort.join(", "),
+ :warning_type => "Attribute Restriction",
+ :message => "Mass assignment is not restricted using attr_accessible",
+ :confidence => CONFIDENCE[:high]
+ end
+ end
+end
98 lib/checks/check_redirect.rb
@@ -0,0 +1,98 @@
+require 'checks/base_check'
+require 'processors/lib/find_call'
+
+#Reports any calls to +redirect_to+ which include parameters in the arguments.
+#
+#For example:
+#
+# redirect_to params.merge(:action => :elsewhere)
+class CheckRedirect < BaseCheck
+ Checks.add self
+
+ def run_check
+ @tracker.find_call(nil, :redirect_to).each do |c|
+ process c
+ end
+ end
+
+ def process_result exp
+ call = exp[-1]
+
+ method = call[2]
+
+ if method == :redirect_to and not only_path?(call) and res = include_user_input?(call)
+ if res == :immediate
+ confidence = CONFIDENCE[:high]
+ else
+ confidence = CONFIDENCE[:low]
+ end
+
+ warn :result => exp,
+ :warning_type => "Redirect",
+ :message => "Possible unprotected redirect",
+ :line => call.line,
+ :code => call,
+ :confidence => confidence
+ end
+
+ exp
+ end
+
+ #Custom check for user input. First looks to see if the user input
+ #is being output directly. This is necessary because of OPTIONS[:check_arguments]
+ #which can be used to enable/disable reporting output of method calls which use
+ #user input as arguments.
+ def include_user_input? call
+
+ if OPTIONS[:ignore_redirect_to_model] and call? call[3][1] and
+ call[3][1][2] == :new and call[3][1][1]
+
+ begin
+ target = class_name call[3][1][1]
+ if @tracker.models.include? target
+ return false
+ end
+ rescue
+ end
+ end
+
+ call[3].each do |arg|
+ if call? arg
+ if ALL_PARAMETERS.include? arg or arg[2] == COOKIES
+ return :immediate
+ elsif arg[2] == :url_for and include_user_input? arg
+ return :immediate
+ #Ignore helpers like some_model_url?
+ elsif arg[2].to_s =~ /_(url|path)$/
+ return false
+ end
+ elsif params? arg or cookies? arg
+ return :immediate
+ end
+ end
+
+ if OPTIONS[:check_arguments]
+ super
+ else
+ false
+ end
+ end
+
+ #Checks +redirect_to+ arguments for +only_path => true+ which essentially
+ #nullifies the danger posed by redirecting with user input
+ def only_path? call
+ call[3].each do |arg|
+ if hash? arg
+ hash_iterate(arg) do |k,v|
+ if symbol? k and k[1] == :only_path and v.is_a? Sexp and v[0] == :true
+ return true
+ end
+ end
+ elsif call? arg and arg[2] == :url_for
+ return only_path?(arg)
+ end
+ end
+
+ false
+ end
+end
65 lib/checks/check_render.rb
@@ -0,0 +1,65 @@
+require 'checks/base_check'
+
+#Check calls to +render()+ for dangerous values
+class CheckRender < BaseCheck
+ Checks.add self
+
+ def run_check
+ tracker.each_method do |src, class_name, method_name|
+ @current_class = class_name
+ @current_method = method_name
+ process src
+ end
+
+ tracker.each_template do |name, template|
+ @current_template = template
+ process template[:src]
+ end
+ end
+
+ def process_render exp
+ case exp[1]
+ when :partial, :template, :action, :file
+ check_for_dynamic_path exp
+ when :inline
+ when :js
+ when :json
+ when :text
+ when :update
+ when :xml
+ end
+ exp
+ end
+
+ #Check if path to action or file is determined dynamically
+ def check_for_dynamic_path exp
+ view = exp[2]
+
+ if sexp? view and view.node_type != :str and view.node_type != :lit and not duplicate? exp
+
+ add_result exp
+
+ if include_user_input? view
+ confidence = CONFIDENCE[:high]
+ else
+ confidence = CONFIDENCE[:low]
+ end
+
+ warning = { :warning_type => "Dynamic Render Path",
+ :message => "Render path is dynamic",
+ :line => exp.line,
+ :code => exp,
+ :confidence => confidence }
+
+
+ if @current_template
+ warning[:template] = @current_template
+ else
+ warning[:class] = @current_class
+ warning[:method] = @current_method
+ end
+
+ warn warning
+ end
+ end
+end
64 lib/checks/check_send.rb
@@ -0,0 +1,64 @@
+require 'checks/base_check'
+require 'processors/lib/find_call'
+
+#This check looks for calls to +send()+ which use user input
+#for or in the method name.
+#
+#For example:
+#
+# some_object.send(params[:input], "hello")
+#
+#This is dangerous for (at least) two reasons. First, the user is controlling
+#what method is being called, which cannot be good. Secondly, the method
+#name will be converted to a Symbol, which is never garbage collected. This
+#could possibly lead to DoS attacks by using up memory.
+class CheckSend < BaseCheck
+ Checks.add self
+
+ #Check calls to +send+ and +__send__+
+ def run_check
+ calls = tracker.find_call nil, [:send, :__send__]
+
+ calls.each do |result|
+ process result
+ end
+ end
+
+ #Check instances of +send+ which have user input in the method name
+ def process_result exp
+ call = exp[-1]
+ method_name = process call[3][1]
+ message = nil
+ confidence = nil
+
+ type, = has_immediate_user_input? method_name
+
+ if type
+ confidence = CONFIDENCE[:high]
+ elsif match = has_immediate_model?(method_name)
+ type = :model
+ confidence = CONFIDENCE[:high]
+ elsif type = include_user_input?(method_name)
+ confidence = CONFIDENCE[:med]
+ end
+
+ if type == :model
+ message = "Database value used to generate method name"
+ elsif type
+ message = "User input used to generate method name"
+ end
+
+ if message and confidence and not duplicate? call
+ add_result call
+
+ warn :result => exp,
+ :warning_type => "Object#send",
+ :message => message,
+ :line => call.line,
+ :code => call,
+ :confidence => confidence
+ end
+
+ exp
+ end
+end
15 lib/checks/check_send_file.rb
@@ -0,0 +1,15 @@
+require 'checks/check_file_access'
+require 'processors/lib/processor_helper'
+
+#Checks for user input in send_file()
+class CheckSendFile < CheckFileAccess
+ Checks.add self
+
+ def run_check
+ methods = tracker.find_call nil, :send_file
+
+ methods.each do |call|
+ process_result call
+ end
+ end
+end
36 lib/checks/check_session_settings.rb
@@ -0,0 +1,36 @@
+require 'checks/base_check'
+
+class CheckSessionSettings < BaseCheck
+ Checks.add self
+
+ def run_check
+ settings = tracker.config[:rails] and
+ tracker.config[:rails][:action_controller] and
+ tracker.config[:rails][:action_controller][:session]
+
+ if settings and hash? settings
+ hash_iterate settings do |key, value|
+ if symbol? key
+
+ if key[1] == :session_http_only and
+ sexp? value and
+ value.node_type == :false
+
+ warn :warning_type => "Session Setting",
+ :message => "Session cookies should be set to HTTP only",
+ :confidence => CONFIDENCE[:high]
+
+ elsif key[1] == :secret and
+ string? value and
+ value[1].length < 30
+
+ warn :warning_type => "Session Setting",
+ :message => "Session secret should be at least 30 characters long",
+ :confidence => CONFIDENCE[:high]
+
+ end
+ end
+ end
+ end
+ end
+end
124 lib/checks/check_sql.rb
@@ -0,0 +1,124 @@
+require 'checks/base_check'
+require 'processors/lib/find_call'
+
+#This check tests for find calls which do not use Rails' auto SQL escaping
+#
+#For example:
+# Project.find(:all, :conditions => "name = '" + params[:name] + "'")
+#
+# Project.find(:all, :conditions => "name = '#{params[:name]}'")
+#
+# User.find_by_sql("SELECT * FROM projects WHERE name = '#{params[:name]}'")
+class CheckSQL < BaseCheck
+ Checks.add self
+
+ def run_check
+ @rails_version = tracker.config[:rails_version]
+ calls = tracker.find_model_find tracker.models.keys
+ calls.concat tracker.find_call([], /^(find.*|last|first|all|count|sum|average|minumum|maximum)$/)
+ calls.each do |c|
+ process c
+ end
+ end
+
+ #Process result from FindCall.
+ def process_result exp
+ call = exp[-1]
+
+ args = process call[3]
+
+ if call[2] == :find_by_sql
+ failed = check_arguments args[1]
+ elsif call[2].to_s =~ /^find/
+ failed = (args.length > 2 and check_arguments args[-1])
+ else
+ failed = (args.length > 1 and check_arguments args[-1])
+ end
+
+ if failed and not duplicate? call, exp[1]
+ add_result call, exp[1]
+
+ if include_user_input? args[-1]
+ confidence = CONFIDENCE[:high]
+ else
+ confidence = CONFIDENCE[:med]
+ end
+
+ warn :result => exp,
+ :warning_type => "SQL Injection",
+ :message => "Possible SQL injection",
+ :confidence => confidence
+ end
+
+ if check_for_limit_or_offset_vulnerability args[-1]
+ if include_user_input? args[-1]
+ confidence = CONFIDENCE[:high]
+ else
+ confidence = CONFIDENCE[:low]
+ end
+
+ warn :result => exp,
+ :warning_type => "SQL Injection",
+ :message => "Upgrade to Rails >= 2.1.2 to escape :limit and :offset. Possible SQL injection",
+ :confidence => confidence
+ end
+ exp
+ end
+
+ private
+
+ #Check arguments for any string interpolation
+ def check_arguments arg
+ if sexp? arg
+ case arg.node_type
+ when :hash
+ hash_iterate(arg) do |key, value|
+ if check_arguments value
+ return true
+ end
+ end
+ when :array
+ return check_arguments(arg[1])
+ when :string_interp
+ return true
+ when :call
+ return check_call(arg)
+ end
+ end
+
+ false
+ end
+
+ #Check call for user input and string building
+ def check_call exp
+ target = exp[1]
+ method = exp[2]
+ args = exp[3]
+ if sexp? target and
+ (method == :+ or method == :<< or method == :concat) and
+ (string? target or include_user_input? exp)
+
+ true
+ elsif call? target
+ check_call target
+ else
+ false
+ end
+ end
+
+ #Prior to Rails 2.1.1, the :offset and :limit parameters were not
+ #escaping input properly.
+ #
+ #http://www.rorsecurity.info/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter/
+ def check_for_limit_or_offset_vulnerability options
+ return false if @rails_version.nil? or @rails_version >= "2.1.1" or not hash? options
+
+ hash_iterate(options) do |key, value|
+ if symbol? key
+ return (key[1] == :limit or key[1] == :offset)
+ end
+ end
+
+ false
+ end
+end
54 lib/checks/check_symbol_creation.rb
@@ -0,0 +1,54 @@
+require 'checks/base_check'
+require 'processors/lib/find_call'
+
+#If an application turns user input into Symbols, there is a possiblity
+#of DoS by using up all the memory.
+class CheckSymbolCreation < BaseCheck
+ Checks.add self
+
+ def run_check
+ calls = tracker.find_call nil, :to_sym
+
+ calls.each do |result|
+ process_call_to_sym result
+ end
+ end
+
+ #Check calls to .to_sym which have user input as a target.
+ def process_call_to_sym exp
+ call = exp[-1]
+ confidence = nil
+
+ type, = has_immediate_user_input? call[1]
+
+ if type
+ confidence = CONFIDENCE[:high]
+ elsif match = has_immediate_model?(call[1])
+ type = :model
+ confidence = CONFIDENCE[:high]
+ elsif type = include_user_input?(call[1])
+ confidence = CONFIDENCE[:med]
+ end
+
+ if type and not duplicate? exp
+ add_result exp
+
+ if res == :model
+ message = "Symbol created from database value"
+ else
+ message = "Symbol created from user input"
+ end
+
+ warn :result => exp,
+ :warning_type => "Symbol Creation",
+ :message => message,
+ :line => call.line,
+ :code => call,
+ :confidence => CONFIDENCE[:med]
+ end
+
+ exp
+
+ end
+
+end
60 lib/checks/check_validation_regex.rb
@@ -0,0 +1,60 @@
+require 'checks/base_check'
+
+#Reports any calls to +validates_format_of+ which do not use +\A+ and +\z+
+#as anchors in the given regular expression.
+#
+#For example:
+#
+# #Allows anything after new line
+# validates_format_of :user_name, :with => /^\w+$/
+class CheckValidationRegex < BaseCheck
+ Checks.add self
+
+ WITH = Sexp.new(:lit, :with)
+
+ def run_check
+ tracker.models.each do |name, model|
+ @current_model = name
+ format_validations = model[:options][:validates_format_of]
+ if format_validations
+ format_validations.each do |v|
+ process_validator v
+ end
+ end
+ end
+ end
+
+ #Check validates_format_of
+ def process_validator validator
+ hash_iterate(validator[-1]) do |key, value|
+ if key == WITH
+ check_regex value, validator
+ end
+ end
+ end
+
+ #Issue warning if the regular expression does not use
+ #+\A+ and +\z+
+ def check_regex value, validator
+ return unless regexp? value
+
+ regex = value[1].inspect
+ if regex =~ /[^\A].*[^\z]\/(m|i|x|n|e|u|s|o)*\z/
+ warn :model => @current_model,
+ :warning_type => "Format Validation",
+ :message => "Insufficient validation for '#{get_name validator}' using #{value[1].inspect}. Use \\A and \\z as anchors",
+ :line => value.line,
+ :confidence => CONFIDENCE[:high]
+ end
+ end
+
+ #Get the name of the attribute being validated.
+ def get_name validator
+ name = validator[1]
+ if sexp? name
+ name[1]
+ else
+ name
+ end
+ end
+end
105 lib/format/style.css
@@ -0,0 +1,105 @@
+/* CSS style used for HTML reports */
+
+body {
+ font-family: sans-serif;
+ color: #161616;
+}
+
+p {
+ font-weight: bold;
+ font-size: 11pt;
+ color: #2D0200;
+ }
+
+ th {
+ background-color: #980905;
+ border-bottom: 5px solid #530200;
+ color: white;
+ font-size: 11pt;
+ padding: 1px 8px 1px 8px;
+ }
+
+ td {
+ border-bottom: 2px solid white;
+ font-family: monospace;
+ padding: 5px 8px 1px 8px;
+ }
+
+ table {
+ background-color: #FCF4D4;
+ border-collapse: collapse;
+ }
+
+ h1 {
+ color: #2D0200;
+ font-size: 14pt;
+ }
+
+ h2 {
+ color: #2D0200;
+ font-size: 12pt;
+ }
+
+ span.high-confidence {
+ font-weight:bold;
+ color: red;
+ }
+
+ span.med-confidence {
+ }
+
+ span.weak-confidence {
+ color:gray;
+ }
+
+ div.warning_message {
+ cursor: pointer;
+ }
+
+ div.warning_message:hover {
+ background-color: white;
+ }
+
+ table.context {
+ margin-top: 5px;
+ margin-bottom: 5px;
+ border-left: 1px solid #90e960;
+ color: #212121;
+ }
+
+ tr.context {
+ background-color: white;
+ }
+
+ tr.first {
+ border-top: 1px solid #7ecc54;
+ padding-top: 2px;
+ }
+
+ tr.error {
+ background-color: #f4c1c1 !important
+ }
+
+ tr.near_error {
+ background-color: #f4d4d4 !important
+ }
+
+ tr.alt {
+ background-color: #e8f4d4;
+ }
+
+ td.context {
+ padding: 2px 10px 0px 6px;
+ border-bottom: none;
+ }
+
+ td.context_line {
+ padding: 2px 8px 0px 7px;
+ border-right: 1px solid #b3bda4;
+ border-bottom: none;
+ color: #6e7465;
+ }
+
+ pre.context {
+ margin-bottom: 1px;
+ }
83 lib/processor.rb
@@ -0,0 +1,83 @@
+#Load all files in processors/
+Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/processors/*.rb").each { |f| require f.match(/processors.*/)[0] }
+require 'tracker'
+require 'set'
+
+#Makes calls to the appropriate processor.
+#
+#The ControllerProcessor, TemplateProcessor, and ModelProcessor will
+#update the Tracker with information about what is parsed.
+class Processor
+ def initialize
+ @tracker = Tracker.new self
+ end
+
+ def tracked_events
+ @tracker
+ end
+
+ #Process configuration file source
+ def process_config src
+ ConfigProcessor.new(@tracker).process_config src
+ end
+
+ #Process route file source
+ def process_routes src
+ RoutesProcessor.new(@tracker).process_routes src
+ end
+
+ #Process controller source. +file_name+ is used for reporting
+ def process_controller src, file_name
+ ControllerProcessor.new(@tracker).process_controller src, file_name
+ end
+
+ #Process variable aliasing in controller source and save it in the
+ #tracker.
+ def process_controller_alias src
+ ControllerAliasProcessor.new(@tracker).process src
+ end
+
+ #Process a model source
+ def process_model src, file_name
+ result = ModelProcessor.new(@tracker).process_model src, file_name
+ AliasProcessor.new.process result
+ end
+
+ #Process either an ERB or HAML template
+ def process_template name, src, type, called_from = nil, file_name = nil
+ case type
+ when :erb
+ result = ErbTemplateProcessor.new(@tracker, name, called_from, file_name).process src
+ when :haml
+ result = HamlTemplateProcessor.new(@tracker, name, called_from, file_name).process src
+ else
+ abort "Unknown template type: #{type} (#{name})"
+ end
+
+ #Each template which is rendered is stored separately
+ #with a new name.
+ if called_from
+ name = (name.to_s + "." + called_from.to_s).to_sym
+ end
+
+ @tracker.templates[name][:src] = result
+ @tracker.templates[name][:type] = type
+ end
+
+ #Process any calls to render() within a template
+ def process_template_alias template
+ TemplateAliasProcessor.new(@tracker, template).process_safely template[:src]
+ end
+
+ #Process source for initializing files
+ def process_initializer name, src
+ res = BaseProcessor.new(@tracker).process src
+ res = AliasProcessor.new.process res
+ @tracker.initializers[name] = res
+ end
+
+ #Process source for a library file
+ def process_lib src, file_name
+ LibraryProcessor.new(@tracker).process_library src, file_name
+ end
+end
384 lib/processors/alias_processor.rb
@@ -0,0 +1,384 @@
+require 'rubygems'
+require 'sexp_processor'
+require 'util'
+require 'processors/lib/processor_helper'
+
+#Returns an s-expression with aliases replaced with their value.
+#This does not preserve semantics (due to side effects, etc.), but it makes
+#processing easier when searching for various things.
+class AliasProcessor < SexpProcessor
+ include ProcessorHelper
+ include Util
+
+ attr_reader :result
+
+ #Returns a new AliasProcessor with an empty environment.
+ #
+ #The recommended usage is:
+ #
+ # AliasProcessor.new.process_safely src
+ def initialize
+ super()
+ self.strict = false
+ self.auto_shift_type = false
+ self.require_empty = false
+ self.default_method = :process_default
+ self.warn_on_default = false
+ @env = SexpProcessor::Environment.new
+ set_env_defaults
+ end
+
+ #This method processes the given Sexp, but copies it first so
+ #the original argument will not be modified.
+ #
+ #_set_env_ should be an instance of SexpProcessor::Environment. If provided,
+ #it will be used as the starting environment.
+ #
+ #This method returns a new Sexp with variables replaced with their values,
+ #where possible.
+ def process_safely src, set_env = nil
+ @env = Marshal.load(Marshal.dump(set_env)) if set_env
+ @result = src.deep_clone
+ process @result
+
+ #Process again to propogate replaced variables and process more.
+ #For example,
+ # x = [1,2]
+ # y = [3,4]
+ # z = x + y
+ #
+ #After first pass:
+ #
+ # z = [1,2] + [3,4]
+ #
+ #After second pass:
+ #
+ # z = [1,2,3,4]
+ if set_env
+ @env = set_env
+ else
+ @env = SexpProcessor::Environment.new
+ end
+
+ process @result
+
+ @result
+ end
+
+ #Process a Sexp. If the Sexp has a value associated with it in the
+ #environment, that value will be returned.
+ def process_default exp
+ begin
+ type = exp.shift
+ exp.each_with_index do |e, i|
+ if sexp? e and not e.empty?
+ exp[i] = process e
+ else
+ e
+ end
+ end
+ rescue Exception => err
+ @tracker.error err if @tracker
+ ensure
+ #The type must be put back on, or else later processing
+ #will trip up on it
+ exp.unshift type
+ end
+
+ #Generic replace
+ if replacement = env[exp]
+ set_line replacement.deep_clone, exp.line
+ else
+ exp
+ end
+ end
+
+ #Process a method call.
+ def process_call exp
+ target_var = exp[1]
+ exp = process_default exp
+
+ #In case it is replaced with something else
+ return exp unless call? exp
+
+ target = exp[1]
+ method = exp[2]
+ args = exp[3]
+
+ #See if it is possible to simplify some basic cases
+ #of addition/concatenation.
+ case method
+ when :+
+ if array? target and array? args[1]
+ joined = join_arrays target, args[1]
+ joined.line(exp.line)
+ exp = joined
+ elsif string? target and string? args[1]
+ joined = join_strings target, args[1]
+ joined.line(exp.line)
+ exp = joined
+ end
+ when :[]
+ if array? target
+ temp_exp = process_array_access target, args[1..-1]
+ exp = temp_exp if temp_exp
+ elsif hash? target
+ temp_exp = process_hash_access target, args[1..-1]
+ exp = temp_exp if temp_exp
+ end
+ when :merge!, :update
+ if hash? target and hash? args[1]
+ target = process_hash_merge! target, args[1]
+ env[target_var] = target
+ return target
+ end
+ when :merge
+ if hash? target and hash? args[1]
+ return process_hash_merge(target, args[1])
+ end
+ end
+
+ exp
+ end
+
+ #Process a new scope.
+ def process_scope exp
+ env.scope do
+ process exp[1]
+ end
+ exp
+ end
+
+ #Start new scope for block.
+ def process_block exp
+ env.scope do
+ process_default exp
+ end
+ end
+
+ #Process a method definition.
+ def process_methdef exp
+ env.scope do
+ set_env_defaults
+ process exp[3]
+ end
+ exp
+ end
+
+ #Process a method definition on self.
+ def process_selfdef exp
+ env.scope do
+ set_env_defaults
+ process exp[4]
+ end
+ exp
+ end
+
+ alias process_defn process_methdef
+ alias process_defs process_selfdef
+
+ #Local assignment
+ # x = 1
+ def process_lasgn exp