Skip to content
This repository
Browse code

Initial release

  • Loading branch information...
commit cd1663fb22bbe2e1097bac8f9ee96fcdadc07fd2 0 parents
Justin authored

Showing 50 changed files with 6,208 additions and 0 deletions. Show diff stats Hide diff stats

  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 @@
  1 +Can detect:
  2 +-Possibly unescaped model attributes or parameters in views (Cross Site Scripting)
  3 +-Bad string interpolation in calls to Model.find, Model.last, Model.first, etc., as well as chained calls (SQL Injection)
  4 +-String interpolation in find_by_sql (SQL Injection)
  5 +-String interpolation or params in calls to system, exec, and syscall and `` (Command Injection)
  6 +-Unrestricted mass assignments
  7 +-Global restriction of mass assignment
  8 +-Missing call to protect_from_forgery in ApplicationController (CSRF protection)
  9 +-Default routes, per-controller and globally
  10 +-Redirects based on params (probably too broad currently)
  11 +-Validation regexes not using \A and \z
  12 +-Calls to render with dynamic paths
  13 +
  14 +General capabilities:
  15 +-Search for method calls based on target class and/or method name
  16 +-Determine 'output' of templates using ERB, Erubis, or HAML. Can handle automatic escaping
21 LICENSE
... ... @@ -0,0 +1,21 @@
  1 +The MIT License
  2 +
  3 +Copyright (c) 2010, YELLOWPAGES.COM, LLC
  4 +
  5 +Permission is hereby granted, free of charge, to any person obtaining a copy
  6 +of this software and associated documentation files (the "Software"), to deal
  7 +in the Software without restriction, including without limitation the rights
  8 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 +copies of the Software, and to permit persons to whom the Software is
  10 +furnished to do so, subject to the following conditions:
  11 +
  12 +The above copyright notice and this permission notice shall be included in
  13 +all copies or substantial portions of the Software.
  14 +
  15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21 +THE SOFTWARE.
112 README.md
Source Rendered
... ... @@ -0,0 +1,112 @@
  1 +# Brakeman
  2 +
  3 +Brakeman is a static analysis tool which checks Ruby on Rails applications for security vulnerabilities.
  4 +
  5 +It targets Rails versions > 2.0 and < 3.0.
  6 +
  7 +# Installation
  8 +
  9 + gem build brakeman.gemspec
  10 + gem install brakeman*.gem
  11 +
  12 +# Usage
  13 +
  14 + brakeman path/to/rails/app/root
  15 +
  16 +# Options
  17 +
  18 +To specify an output file for the results:
  19 +
  20 + brakeman -o output_file path/to/rails/app/root
  21 +
  22 +The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, and `csv`.
  23 +
  24 +To suppress informational warnings and just output the report:
  25 +
  26 + brakeman -q path/to/rails/app/root
  27 +
  28 +To see all kinds of debugging information:
  29 +
  30 + brakeman -d path/to/rails/app/root
  31 +
  32 +Specific checks can be skipped, if desired. The name needs to be the correct case. For example, to skip looking for default routes (`DefaultRoutes`):
  33 +
  34 + brakeman -x DefaultRoutes path/to/rails/app/root
  35 +
  36 +Multiple checks should be separated by a comma:
  37 +
  38 + brakeman -x DefaultRoutes,Redirect path/to/rails/app/root
  39 +
  40 +To do the opposite and only run a certain set of tests:
  41 +
  42 + brakeman -t Find,ValidationRegex path/to/rails/app/root
  43 +
  44 +To indicate certain methods are "safe":
  45 +
  46 + brakeman -s benign_method,totally_safe path/to/rails/app/root
  47 +
  48 +By default, brakeman will assume that unknown methods involving untrusted data are dangerous. For example, this would a warning:
  49 +
  50 + <%= some_method(:option => params[:input]) %>
  51 +
  52 +To only raise warnings only when untrusted data is being directly used:
  53 +
  54 + brakeman -r path/to/rails/app/root
  55 +
  56 +# Warning information
  57 +
  58 +See WARNING_TYPES for more information on the warnings reported by this tool.
  59 +
  60 +# Warning context
  61 +
  62 +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.
  63 +
  64 +# Confidence levels
  65 +
  66 +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.
  67 +
  68 +There are three levels of confidence:
  69 +
  70 + + High - Either this is a simple warning (boolean value) or user input is very likely being used in unsafe ways.
  71 + + Medium - This generally indicates an unsafe use of a variable, but the variable may or may not be user input.
  72 + + Weak - Typically means user input was indirectly used in a potentially unsafe manner.
  73 +
  74 +To only get warnings above a given confidence level:
  75 +
  76 + brakeman -w3 /path/to/rails/app/root
  77 +
  78 +The `-w` switch takes a number from 1 to 3, with 1 being low (all warnings) and 3 being high (only high confidence warnings).
  79 +
  80 +# Configuration files
  81 +
  82 +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.
  83 +
  84 +Options passed in on the commandline have priority over configuration files.
  85 +
  86 +The default config locations are `./config.yaml`, `~/.brakeman/`, and `/etc/brakeman/config.yaml`
  87 +
  88 +The `-c` option can be used to specify a configuration file to use.
  89 +
  90 +# License
  91 +
  92 +The MIT License
  93 +
  94 +Copyright (c) 2010, YELLOWPAGES.COM, LLC
  95 +
  96 +Permission is hereby granted, free of charge, to any person obtaining a copy
  97 +of this software and associated documentation files (the "Software"), to deal
  98 +in the Software without restriction, including without limitation the rights
  99 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  100 +copies of the Software, and to permit persons to whom the Software is
  101 +furnished to do so, subject to the following conditions:
  102 +
  103 +The above copyright notice and this permission notice shall be included in
  104 +all copies or substantial portions of the Software.
  105 +
  106 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  107 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  108 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  109 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  110 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  111 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  112 +THE SOFTWARE.
69 WARNING_TYPES
... ... @@ -0,0 +1,69 @@
  1 +This file describes the various warning types reported by this tool.
  2 +
  3 +# Cross Site Scripting
  4 +
  5 +Cross site scripting warnings are raised when a parameter or model attribute is output through a view without being escaped.
  6 +
  7 +See http://guides.rubyonrails.org/security.html#cross-site-scripting-xss for details.
  8 +
  9 +# SQL Injection
  10 +
  11 +String interpolation or concatenation has been detected in an SQL query. Use parameterized queries instead.
  12 +
  13 +See http://guides.rubyonrails.org/security.html#sql-injection for details.
  14 +
  15 +# Command Injection
  16 +
  17 +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.
  18 +
  19 +See http://guides.rubyonrails.org/security.html#command-line-injection for details.
  20 +
  21 +# Mass Assignment
  22 +
  23 +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.
  24 +
  25 +Mass assignment can be disabled globally.
  26 +
  27 +Please see http://railspikes.com/2008/9/22/is-your-rails-application-safe-from-mass-assignment for more details.
  28 +
  29 +# Attribute Restriction
  30 +
  31 +This warning comes up if a model does not limit what attributes can be set through mass assignment.
  32 +
  33 +In particular, this check looks for `attr_accessible` inside model definitions. If it is not found, this warning will be issued.
  34 +
  35 +Note that disabling mass assignment globally will suppress these warnings.
  36 +
  37 +# Cross-Site Request Forgery
  38 +
  39 +No call to `protect_from_forgery` was found in `ApplicationController`. This method prevents CSRF.
  40 +
  41 +See http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf for details.
  42 +
  43 +# Redirect
  44 +
  45 +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.
  46 +
  47 +This warning is shown when request parameters are used inside a call to `redirect_to`.
  48 +
  49 +See http://www.owasp.org/index.php/Top_10_2010-A10 for more information.
  50 +
  51 +# Default Routes
  52 +
  53 +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.
  54 +
  55 +If this warning is reported for a particular controller, it means there is a route to that controller containing `:action`.
  56 +
  57 +Default routes can be dangerous if methods are made public which are not intended to be used as URLs or actions.
  58 +
  59 +# Format Validation
  60 +
  61 +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.
  62 +
  63 +See http://guides.rubyonrails.org/security.html#regular-expressions for details.
  64 +
  65 +# Dynamic Render Path
  66 +
  67 +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.
  68 +
  69 +This warning is shown whenever the path to be rendered is not a static string or symbol.
266 bin/brakeman
... ... @@ -0,0 +1,266 @@
  1 +#!/usr/bin/env ruby
  2 +require "optparse"
  3 +require 'set'
  4 +require 'yaml'
  5 +
  6 +Version = "0.0.1"
  7 +
  8 +trap("INT") do
  9 + $stderr.puts "\nInterrupted - exiting."
  10 + exit!
  11 +end
  12 +
  13 +#Load scanner
  14 +begin
  15 + require 'scanner'
  16 +rescue LoadError
  17 + #Try to find lib directory locally
  18 + $: << "#{File.expand_path(File.dirname(__FILE__))}/../lib"
  19 +
  20 + begin
  21 + require 'scanner'
  22 + rescue LoadError
  23 + abort "Cannot find lib/ directory."
  24 + end
  25 +end
  26 +
  27 +#Parse command line options
  28 +options = {}
  29 +
  30 +OptionParser.new do |opts|
  31 + opts.banner = "Usage: brakeman [options] rails/root/path"
  32 +
  33 + opts.on "-p", "--path PATH", "Specify path to Rails application" do |path|
  34 + options[:app_path] = File.expand_path path
  35 + end
  36 +
  37 + opts.on "-q", "--quiet", "Suppress informational messages" do
  38 + options[:quiet] = true
  39 + $VERBOSE = nil
  40 + end
  41 +
  42 + opts.separator ""
  43 + opts.separator "Scanning options:"
  44 +
  45 + opts.on "--ignore-model-output", "Consider model attributes XSS-safe" do
  46 + options[:ignore_model_output] = true
  47 + end
  48 +
  49 + opts.on "-r", "--report-direct", "Only report direct use of untrusted data" do |option|
  50 + options[:check_arguments] = !option
  51 + end
  52 +
  53 + opts.on "-s", "--safe-methods meth1,meth2,etc", Array, "Consider the specified methods safe" do |methods|
  54 + options[:safe_methods] ||= Set.new
  55 + options[:safe_methods].merge methods.map {|e| e.to_sym }
  56 + end
  57 +
  58 + opts.on "-t", "--test Check1,Check2,etc", Array, "Only run the specified checks" do |checks|
  59 + checks.each_with_index do |s, index|
  60 + if s[0,5] != "Check"
  61 + checks[index] = "Check" << s
  62 + end
  63 + end
  64 +
  65 + options[:run_checks] ||= Set.new
  66 + options[:run_checks].merge checks
  67 + end
  68 +
  69 + opts.on "-x", "--except Check1,Check2,etc", Array, "Skip the specified checks" do |skip|
  70 + skip.each do |s|
  71 + if s[0,5] != "Check"
  72 + s = "Check" << s
  73 + end
  74 +
  75 + options[:skip_checks] ||= Set.new
  76 + options[:skip_checks] << s
  77 + end
  78 + end
  79 +
  80 + opts.separator ""
  81 + opts.separator "Output options:"
  82 +
  83 + opts.on "-d", "--debug", "Lots of output" do
  84 + options[:debug] = true
  85 + end
  86 +
  87 + opts.on "-f",
  88 + "--format TYPE",
  89 + [:pdf, :text, :html, :csv],
  90 + "Specify output format. Default is text" do |type|
  91 +
  92 + type = "s" if type == :text
  93 + options[:output_format] = ("to_" << type.to_s).to_sym
  94 + end
  95 +
  96 + opts.on "-l", "--[no]-combine-locations", "Combine warning locations (Default)" do |combine|
  97 + options[:combine_locations] = combine
  98 + end
  99 +
  100 + opts.on "-m", "--routes", "Report controller information" do
  101 + options[:report_routes] = true
  102 + end
  103 +
  104 + opts.on "--message-limit LENGTH", "Limit message length in HTML report" do |limit|
  105 + options[:message_limit] = limit.to_i
  106 + end
  107 +
  108 + opts.on "-o", "--output FILE", "Specify file for output. Defaults to stdout" do |file|
  109 + options[:output_file] = file
  110 + end
  111 +
  112 + opts.on "-w",
  113 + "--confidence-level LEVEL",
  114 + ["1", "2", "3"],
  115 + "Set minimal confidence level (1 - 3). Default: 1" do |level|
  116 +
  117 + options[:min_confidence] = 3 - level.to_i
  118 + end
  119 +
  120 + opts.separator ""
  121 + opts.separator "Configuration files:"
  122 +
  123 + opts.on "-c", "--config-file FILE", "Use specified configuration file" do |file|
  124 + options[:config_file] = File.expand_path(file)
  125 + end
  126 +
  127 + opts.on "-C", "--create-config [FILE]", "Output configuration file based on options" do |file|
  128 + if file
  129 + options[:create_config] = file
  130 + else
  131 + options[:create_config] = true
  132 + end
  133 + end
  134 +
  135 + opts.separator ""
  136 +
  137 + opts.on_tail "-h", "--help", "Display this message" do
  138 + puts opts
  139 + exit
  140 + end
  141 +end.parse!(ARGV)
  142 +
  143 +#Load configuation file
  144 +[File.expand_path(options[:config_file].to_s),
  145 + File.expand_path("./config.yaml"),
  146 + File.expand_path("~/.brakeman/config.yaml"),
  147 + File.expand_path("/etc/brakeman/config.yaml"),
  148 + "#{File.expand_path(File.dirname(__FILE__))}/../lib/config.yaml"].each do |f|
  149 +
  150 + if File.exist? f and not File.directory? f
  151 + warn "[Notice] Using configuration in #{f}" unless options[:quiet]
  152 + OPTIONS = YAML.load_file f
  153 + OPTIONS.merge! options
  154 + OPTIONS.each do |k,v|
  155 + if v.is_a? Array
  156 + OPTIONS[k] = Set.new v
  157 + end
  158 + end
  159 + break
  160 + end
  161 +end
  162 +
  163 +OPTIONS = options unless defined? OPTIONS
  164 +
  165 +#Set defaults just in case
  166 +{ :skip_checks => Set.new,
  167 + :check_arguments => true,
  168 + :safe_methods => Set.new,
  169 + :min_confidence => 2,
  170 + :combine_locations => true,
  171 + :collapse_mass_assignment => true,
  172 + :ignore_redirect_to_model => true,
  173 + :ignore_model_output => false,
  174 + :message_limit => 100,
  175 + :html_style => "#{File.expand_path(File.dirname(__FILE__))}/../lib/format/style.css"
  176 +}.each do |k,v|
  177 + OPTIONS[k] = v if OPTIONS[k].nil?
  178 +end
  179 +
  180 +
  181 +#Set output format
  182 +if OPTIONS[:output_format]
  183 + case OPTIONS[:output_format]
  184 + when :html, :to_html
  185 + OPTIONS[:output_format] = :to_html
  186 + when :csv, :to_csv
  187 + OPTIONS[:output_format] = :to_csv
  188 + when :pdf, :to_pdf
  189 + OPTIONS[:output_format] = :to_pdf
  190 + else
  191 + OPTIONS[:output_format] = :to_s
  192 + end
  193 +else
  194 + case OPTIONS[:output_file]
  195 + when /\.html$/i
  196 + OPTIONS[:output_format] = :to_html
  197 + when /\.csv$/i
  198 + OPTIONS[:output_format] = :to_csv
  199 + when /\.pdf$/i
  200 + OPTIONS[:output_format] = :to_pdf
  201 + else
  202 + OPTIONS[:output_format] = :to_s
  203 + end
  204 +end
  205 +
  206 +#Output configuration if requested
  207 +if OPTIONS[:create_config]
  208 +
  209 + if OPTIONS[:create_config].is_a? String
  210 + file = OPTIONS[:create_config]
  211 + else
  212 + file = nil
  213 + end
  214 +
  215 + OPTIONS.delete :create_config
  216 +
  217 + OPTIONS.each do |k,v|
  218 + if v.is_a? Set
  219 + OPTIONS[k] = v.to_a
  220 + end
  221 + end
  222 +
  223 + if file
  224 + File.open file, "w" do |f|
  225 + YAML.dump OPTIONS, f
  226 + end
  227 + puts "Output configuration to #{file}"
  228 + else
  229 + puts YAML.dump(OPTIONS)
  230 + end
  231 + exit
  232 +end
  233 +
  234 +
  235 +#Check application path
  236 +unless OPTIONS[:app_path]
  237 + if ARGV[-1].nil?
  238 + OPTIONS[:app_path] = File.expand_path "."
  239 + else
  240 + OPTIONS[:app_path] = File.expand_path ARGV[-1]
  241 + end
  242 +end
  243 +
  244 +app_path = OPTIONS[:app_path]
  245 +
  246 +abort("Please supply the path to a Rails application.") unless app_path and File.exist? app_path + "/app"
  247 +
  248 +warn "[Notice] Using Ruby #{RUBY_VERSION}. Please make sure this matches the one used to run your Rails application."
  249 +
  250 +#Start scanning
  251 +scanner = Scanner.new app_path
  252 +
  253 +warn "Processing application in #{app_path}"
  254 +tracker = scanner.process
  255 +
  256 +warn "Running checks..."
  257 +tracker.run_checks
  258 +
  259 +warn "Generating report..."
  260 +if OPTIONS[:output_file]
  261 + File.open OPTIONS[:output_file], "w" do |f|
  262 + f.puts tracker.report.send(OPTIONS[:output_format])
  263 + end
  264 +else
  265 + puts tracker.report.send(OPTIONS[:output_format])
  266 +end
16 brakeman.gemspec
... ... @@ -0,0 +1,16 @@
  1 +Gem::Specification.new do |s|
  2 + s.name = %q{brakeman}
  3 + s.version = "0.0.1"
  4 + s.authors = ["Justin Collins"]
  5 + s.email = "justin@presidentbeef.com"
  6 + s.summary = "Security vulnerability scanner for Ruby on Rails."
  7 + s.description = "Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis."
  8 + s.homepage = "http://github.com/presidentbeef/brakeman"
  9 + s.files = ["bin/brakeman", "WARNING_TYPES", "FEATURES", "README.md"] + Dir["lib/**/*.rb"] + Dir["lib/format/*.css"]
  10 + s.executables = ["brakeman"]
  11 + s.add_dependency "activesupport", "~>2.2.2"
  12 + s.add_dependency "ruby2ruby", "~>1.2.4"
  13 + s.add_dependency "ruport", "~>1.6.3"
  14 + s.add_dependency "erubis", "~>2.6.5"
  15 + s.add_dependency "haml", "~>3.0.12"
  16 +end
67 lib/checks.rb
... ... @@ -0,0 +1,67 @@
  1 +#Collects up results from running different checks.
  2 +#
  3 +#Checks can be added with +Check.add(check_class)+
  4 +#
  5 +#All .rb files in checks/ will be loaded.
  6 +class Checks
  7 + @checks = []
  8 +
  9 + attr_reader :warnings, :controller_warnings, :model_warnings, :template_warnings, :checks_run
  10 +
  11 + #Add a check. This will call +_klass_.new+ when running tests
  12 + def self.add klass
  13 + @checks << klass
  14 + end
  15 +
  16 + #No need to use this directly.
  17 + def initialize
  18 + @warnings = []
  19 + @template_warnings = []
  20 + @model_warnings = []
  21 + @controller_warnings = []
  22 + @checks_run = []
  23 + end
  24 +
  25 + #Add Warning to list of warnings to report.
  26 + #Warnings are split into four different arrays
  27 + #for template, controller, model, and generic warnings.
  28 + def add_warning warning
  29 + case warning.warning_set
  30 + when :template
  31 + @template_warnings << warning
  32 + when :warning
  33 + @warnings << warning
  34 + when :controller
  35 + @controller_warnings << warning
  36 + when :model
  37 + @model_warnings << warning
  38 + else
  39 + raise "Unknown warning: #{warning.warning_set}"
  40 + end
  41 + end
  42 +
  43 + #Run all the checks on the given Tracker.
  44 + #Returns a new instance of Checks with the results.
  45 + def self.run_checks tracker
  46 + checks = self.new
  47 + @checks.each do |c|
  48 + #Run or don't run check based on options
  49 + unless OPTIONS[:skip_checks].include? c.to_s or
  50 + (OPTIONS[:run_checks] and not OPTIONS[:run_checks].include? c.to_s)
  51 +
  52 + warn " - #{c}"
  53 + c.new(checks, tracker).run_check
  54 +
  55 + #Maintain list of which checks were run
  56 + #mainly for reporting purposes
  57 + checks.checks_run << c.to_s[5..-1]
  58 + end
  59 + end
  60 + checks
  61 + end
  62 +end
  63 +
  64 +#Load all files in checks/ directory
  65 +Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/checks/*.rb").sort.each do |f|
  66 + require f.match(/(checks\/.*)\.rb$/)[0]
  67 +end
338 lib/checks/base_check.rb
... ... @@ -0,0 +1,338 @@
  1 +require 'rubygems'
  2 +require 'sexp_processor'
  3 +require 'processors/output_processor'
  4 +require 'warning'
  5 +require 'util'
  6 +
  7 +#Basis of vulnerability checks.
  8 +class BaseCheck < SexpProcessor
  9 + include ProcessorHelper
  10 + include Util
  11 + attr_reader :checks, :tracker
  12 +
  13 + CONFIDENCE = { :high => 0, :med => 1, :low => 2 }
  14 +
  15 + #Initialize Check with Checks.
  16 + def initialize checks, tracker
  17 + super()
  18 + @results = [] #only to check for duplicates
  19 + @checks = checks
  20 + @tracker = tracker
  21 + @string_interp = false
  22 + @current_template = @current_module = @current_class = @current_method = nil
  23 + self.strict = false
  24 + self.auto_shift_type = false
  25 + self.require_empty = false
  26 + self.default_method = :process_default
  27 + self.warn_on_default = false
  28 + end
  29 +
  30 + #Add result to result list, which is used to check for duplicates
  31 + def add_result result, location = nil
  32 + location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
  33 + location = location[:name] if location.is_a? Hash
  34 + location = location.to_sym
  35 +
  36 + @results << [result.line, location, result]
  37 + end
  38 +
  39 + #Default Sexp processing. Iterates over each value in the Sexp
  40 + #and processes them if they are also Sexps.
  41 + def process_default exp
  42 + type = exp.shift
  43 + exp.each_with_index do |e, i|
  44 + if sexp? e
  45 + process e
  46 + else
  47 + e
  48 + end
  49 + end
  50 +
  51 + exp.unshift type
  52 + end
  53 +
  54 + #Process calls and check if they include user input
  55 + def process_call exp
  56 + process exp[1] if sexp? exp[1]
  57 + process exp[3]
  58 +
  59 + if ALL_PARAMETERS.include? exp[1] or ALL_PARAMETERS.include? exp or params? exp[1]
  60 + @has_user_input = :params
  61 + elsif exp[1] == COOKIES or exp == COOKIES or cookies? exp[1]
  62 + @has_user_input = :cookies
  63 + elsif sexp? exp[1] and model_name? exp[1][1]
  64 + @has_user_input = :model
  65 + end
  66 +
  67 + exp
  68 + end
  69 +
  70 + #Note that params are included in current expression
  71 + def process_params exp
  72 + @has_user_input = :params
  73 + exp
  74 + end
  75 +
  76 + #Note that cookies are included in current expression
  77 + def process_cookies exp
  78 + @has_user_input = :cookies
  79 + exp
  80 + end
  81 +
  82 + private
  83 +
  84 + #Report a warning
  85 + def warn options
  86 + @checks.add_warning Warning.new(options.merge({ :check => self.class.to_s }))
  87 + end
  88 +
  89 + #Run _exp_ through OutputProcessor to get a nice String.
  90 + def format_output exp
  91 + OutputProcessor.new.format(exp).gsub(/\r|\n/, "")
  92 + end
  93 +
  94 + #Checks if the model inherits from parent,
  95 + def parent? tracker, model, parent
  96 + if model == nil
  97 + false
  98 + elsif model[:parent] == parent
  99 + true
  100 + elsif model[:parent]
  101 + parent? tracker, tracker.models[model[:parent]], parent
  102 + else
  103 + false
  104 + end
  105 + end
  106 +
  107 + #Checks if mass assignment is disabled globally in an initializer.
  108 + def mass_assign_disabled? tracker
  109 + matches = tracker.check_initializers(:"ActiveRecord::Base", :send)
  110 + if matches.empty?
  111 + false
  112 + else
  113 + matches.each do |result|
  114 + if result[3][3] == Sexp.new(:arg_list, Sexp.new(:lit, :attr_accessible), Sexp.new(:nil))
  115 + return true
  116 + end
  117 + end
  118 + end
  119 + end
  120 +
  121 + #This is to avoid reporting duplicates. Checks if the result has been
  122 + #reported already from the same line number.
  123 + def duplicate? result, location = nil
  124 + line = result.line
  125 + location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
  126 +
  127 + location = location[:name] if location.is_a? Hash
  128 + location = location.to_sym
  129 + @results.each do |r|
  130 + if r[0] == line and r[1] == location
  131 + if OPTIONS[:combine_locations]
  132 + return true
  133 + elsif r[2] == result
  134 + return true
  135 + end
  136 + end
  137 + end
  138 +
  139 + false
  140 + end
  141 +
  142 + #Ignores ignores
  143 + def process_ignore exp
  144 + exp
  145 + end
  146 +
  147 + #Does not actually process string interpolation, but notes that it occurred.
  148 + def process_string_interp exp
  149 + @string_interp = true
  150 + exp
  151 + end
  152 +
  153 + #Checks if an expression contains string interpolation.
  154 + def include_interp? exp
  155 + @string_interp = false
  156 + process exp
  157 + @string_interp
  158 + end
  159 +
  160 + #Checks if _exp_ includes parameters or cookies, but this only works
  161 + #with the base process_default.
  162 + def include_user_input? exp
  163 + @has_user_input = false
  164 + process exp
  165 + @has_user_input
  166 + end
  167 +
  168 + #This is used to check for user input being used directly.
  169 + #
  170 + #Returns false if none is found, otherwise it returns an array
  171 + #where the first element is the type of user input
  172 + #(either :params or :cookies) and the second element is the matching
  173 + #expression
  174 + def has_immediate_user_input? exp
  175 + if params? exp
  176 + return :params, exp
  177 + elsif cookies? exp
  178 + return :cookies, exp
  179 + elsif call? exp
  180 + if sexp? exp[1]
  181 + if ALL_PARAMETERS.include? exp[1] or params? exp[1]
  182 + return :params, exp
  183 + elsif exp[1] == COOKIES
  184 + return :cookies, exp
  185 + else
  186 + false
  187 + end
  188 + else
  189 + false
  190 + end
  191 + elsif sexp? exp
  192 + case exp.node_type
  193 + when :string_interp
  194 + exp.each do |e|
  195 + if sexp? e
  196 + type, match = has_immediate_user_input?(e)
  197 + if type
  198 + return type, match
  199 + end
  200 + end
  201 + end
  202 + false
  203 + when :string_eval
  204 + if sexp? exp[1]
  205 + if exp[1].node_type == :rlist
  206 + exp[1].each do |e|
  207 + if sexp? e
  208 + type, match = has_immediate_user_input?(e)
  209 + if type
  210 + return type, match
  211 + end
  212 + end
  213 + end
  214 + false
  215 + else
  216 + has_immediate_user_input? exp[1]
  217 + end
  218 + end
  219 + when :format
  220 + has_immediate_user_input? exp[1]
  221 + when :if
  222 + (sexp? exp[2] and has_immediate_user_input? exp[2]) or
  223 + (sexp? exp[3] and has_immediate_user_input? exp[3])
  224 + else
  225 + false
  226 + end
  227 + end
  228 + end
  229 +
  230 + #Checks for a model attribute at the top level of the
  231 + #expression.
  232 + def has_immediate_model? exp, out = nil
  233 + out = exp if out.nil?
  234 +
  235 + if sexp? exp and exp.node_type == :output
  236 + exp = exp[1]
  237 + end
  238 +
  239 + if call? exp
  240 + target = exp[1]
  241 + method = exp[2]
  242 +
  243 + if call? target and not method.to_s[-1,1] == "?"
  244 + has_immediate_model? target, out
  245 + elsif model_name? target
  246 + exp
  247 + else
  248 + false
  249 + end
  250 + elsif sexp? exp
  251 + case exp.node_type
  252 + when :string_interp
  253 + exp.each do |e|
  254 + if sexp? e and match = has_immediate_model?(e, out)
  255 + return match
  256 + end
  257 + end
  258 + false
  259 + when :string_eval
  260 + if sexp? exp[1]
  261 + if exp[1].node_type == :rlist
  262 + exp[1].each do |e|
  263 + if sexp? e and match = has_immediate_model?(e, out)
  264 + return match
  265 + end
  266 + end
  267 + false
  268 + else
  269 + has_immediate_model? exp[1], out
  270 + end
  271 + end
  272 + when :format
  273 + has_immediate_model? exp[1], out
  274 + when :if
  275 + ((sexp? exp[2] and has_immediate_model? exp[2], out) or
  276 + (sexp? exp[3] and has_immediate_model? exp[3], out))
  277 + else
  278 + false
  279 + end
  280 + end
  281 + end
  282 +
  283 + #Checks if +exp+ is a model name.
  284 + #
  285 + #Prior to using this method, either @tracker must be set to
  286 + #the current tracker, or else @models should contain an array of the model
  287 + #names, which is available via tracker.models.keys
  288 + def model_name? exp
  289 + @models ||= @tracker.models.keys
  290 +
  291 + if exp.is_a? Symbol
  292 + @models.include? exp
  293 + elsif sexp? exp
  294 + klass = nil
  295 + begin
  296 + klass = class_name exp
  297 + rescue StandardError
  298 + end
  299 +
  300 + klass and @models.include? klass
  301 + else
  302 + false
  303 + end
  304 + end
  305 +
  306 + #Finds entire method call chain where +target+ is a target in the chain
  307 + def find_chain exp, target
  308 + return unless sexp? exp
  309 +
  310 + case exp.node_type
  311 + when :output, :format
  312 + find_chain exp[1], target
  313 + when :call
  314 + if exp == target or include_target? exp, target
  315 + return exp
  316 + end
  317 + else
  318 + exp.each do |e|
  319 + if sexp? e
  320 + res = find_chain e, target
  321 + return res if res
  322 + end
  323 + end
  324 + nil
  325 + end
  326 + end
  327 +
  328 + #Returns true if +target+ is in +exp+
  329 + def include_target? exp, target
  330 + return false unless call? exp
  331 +
  332 + exp.each do |e|
  333 + return true if e == target or include_target? e, target
  334 + end
  335 +
  336 + false
  337 + end
  338 +end
216 lib/checks/check_cross_site_scripting.rb
... ... @@ -0,0 +1,216 @@
  1 +require 'checks/base_check'
  2 +require 'processors/lib/find_call'
  3 +require 'processors/lib/processor_helper'
  4 +require 'util'
  5 +require 'set'
  6 +
  7 +#This check looks for unescaped output in templates which contains
  8 +#parameters or model attributes.
  9 +#
  10 +#For example:
  11 +#
  12 +# <%= User.find(:id).name %>
  13 +# <%= params[:id] %>
  14 +class CheckCrossSiteScripting < BaseCheck
  15 + Checks.add self
  16 +
  17 + #Ignore these methods and their arguments.
  18 + #It is assumed they will take care of escaping their output.
  19 + IGNORE_METHODS = Set.new([:h, :escapeHTML, :link_to, :text_field_tag, :hidden_field_tag,
  20 + :image_tag, :select, :submit_tag, :hidden_field, :url_encode,
  21 + :radio_button, :will_paginate, :button_to, :url_for, :mail_to,
  22 + :fields_for, :label, :text_area, :text_field, :hidden_field, :check_box,
  23 + :field_field])
  24 +
  25 + IGNORE_MODEL_METHODS = Set.new([:average, :count, :maximum, :minimum, :sum])
  26 +
  27 + MODEL_METHODS = Set.new([:all, :find, :first, :last, :new])
  28 +
  29 + IGNORE_LIKE = /^link_to_|_path|_tag|_url$/
  30 +
  31 + HAML_HELPERS = Sexp.new(:colon2, Sexp.new(:const, :Haml), :Helpers)
  32 +
  33 + URI = Sexp.new(:const, :URI)
  34 +
  35 + CGI = Sexp.new(:const, :CGI)
  36 +
  37 + FORM_BUILDER = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new, Sexp.new(:arglist))
  38 +
  39 + #Run check
  40 + def run_check
  41 + IGNORE_METHODS.merge OPTIONS[:safe_methods]
  42 + @models = tracker.models.keys
  43 + @inspect_arguments = OPTIONS[:check_arguments]
  44 +
  45 + tracker.each_template do |name, template|
  46 + @current_template = template
  47 +
  48 + template[:outputs].each do |out|
  49 + type, match = has_immediate_user_input?(out[1])
  50 + if type
  51 + unless duplicate? out
  52 + add_result out
  53 + case type
  54 + when :params
  55 +
  56 + warn :template => @current_template,
  57 + :warning_type => "Cross Site Scripting",
  58 + :message => "Unescaped parameter value",
  59 + :line => match.line,
  60 + :code => match,
  61 + :confidence => CONFIDENCE[:high]
  62 +
  63 + when :cookies
  64 +
  65 + warn :template => @current_template,
  66 + :warning_type => "Cross Site Scripting",
  67 + :message => "Unescaped cookie value",
  68 + :line => match.line,
  69 + :code => match,
  70 + :confidence => CONFIDENCE[:high]
  71 + end
  72 + end
  73 + elsif not OPTIONS[:ignore_model_output] and match = has_immediate_model?(out[1])
  74 + method = match[2]
  75 +
  76 + unless duplicate? out or IGNORE_MODEL_METHODS.include? method
  77 + add_result out
  78 +
  79 + if MODEL_METHODS.include? method or method.to_s =~ /^find_by/
  80 + confidence = CONFIDENCE[:high]
  81 + else
  82 + confidence = CONFIDENCE[:med]
  83 + end
  84 +
  85 + code = find_chain out, match
  86 + warn :template => @current_template,
  87 + :warning_type => "Cross Site Scripting",
  88 + :message => "Unescaped model attribute",
  89 + :line => code.line,
  90 + :code => code,
  91 + :confidence => confidence
  92 + end
  93 +
  94 + else
  95 + @matched = false
  96 + @mark = false
  97 + process out
  98 + end
  99 + end
  100 + end
  101 + end
  102 +
  103 + #Process an output Sexp
  104 + def process_output exp
  105 + process exp[1]
  106 + end
  107 +
  108 + #Check a call for user input
  109 + #
  110 + #
  111 + #Since we want to report an entire call and not just part of one, use @mark
  112 + #to mark when a call is started. Any dangerous values inside will then
  113 + #report the entire call chain.
  114 + def process_call exp
  115 + if @mark
  116 + actually_process_call exp
  117 + else
  118 + @mark = true
  119 + actually_process_call exp
  120 + message = nil
  121 +
  122 + if @matched == :model and not OPTIONS[:ignore_model_output]
  123 + message = "Unescaped model attribute"
  124 + elsif @matched == :params
  125 + message = "Unescaped parameter value"
  126 + end
  127 +
  128 + if message and not duplicate? exp
  129 + add_result exp
  130 +
  131 + warn :template => @current_template,
  132 + :warning_type => "Cross Site Scripting",
  133 + :message => message,
  134 + :line => exp.line,
  135 + :code => exp,
  136 + :confidence => CONFIDENCE[:low]
  137 + end
  138 +
  139 + @mark = @matched = false
  140 + end
  141 +
  142 + exp
  143 + end
  144 +
  145 + def actually_process_call exp
  146 + return if @matched
  147 + target = exp[1]
  148 + if sexp? target
  149 + target = process target
  150 + end
  151 +
  152 + method = exp[2]
  153 + args = exp[3]
  154 +
  155 + #Ignore safe items
  156 + if (target.nil? and (IGNORE_METHODS.include? method or method.to_s =~ IGNORE_LIKE)) or
  157 + (@matched == :model and IGNORE_MODEL_METHODS.include? method) or
  158 + (target == HAML_HELPERS and method == :html_escape) or
  159 + ((target == URI or target == CGI) and method == :escape) or
  160 + (target == FORM_BUILDER and IGNORE_METHODS.include? method) or
  161 + (method.to_s[-1,1] == "?")
  162 +
  163 + exp[0] = :ignore
  164 + @matched = false
  165 + elsif sexp? exp[1] and model_name? exp[1][1]
  166 +
  167 + @matched = :model
  168 + elsif @inspect_arguments and (ALL_PARAMETERS.include?(exp) or params? exp)
  169 +
  170 + @matched = :params
  171 + else
  172 + process args if @inspect_arguments
  173 + end
  174 + end
  175 +
  176 + #Note that params have been found
  177 + def process_params exp
  178 + @matched = :params
  179 + exp
  180 + end
  181 +
  182 + #Note that cookies have been found
  183 + def process_cookies exp
  184 + @matched = :cookies
  185 + exp
  186 + end
  187 +
  188 + #Ignore calls to render
  189 + def process_render exp
  190 + exp
  191 + end
  192 +
  193 + #Process as default
  194 + def process_string_interp exp
  195 + process_default exp
  196 + end
  197 +
  198 + #Process as default
  199 + def process_format exp
  200 + process_default exp
  201 + end
  202 +
  203 + #Ignore output HTML escaped via HAML
  204 + def process_format_escaped exp
  205 + exp
  206 + end
  207 +
  208 + #Ignore condition in if Sexp
  209 + def process_if exp
  210 + exp[2..-1].each do |e|
  211 + process e if sexp? e
  212 + end
  213 + exp
  214 + end
  215 +
  216 +end
29 lib/checks/check_default_routes.rb
... ... @@ -0,0 +1,29 @@
  1 +require 'checks/base_check'
  2 +
  3 +#Checks if default routes are allowed in routes.rb
  4 +class CheckDefaultRoutes < BaseCheck
  5 + Checks.add self
  6 +
  7 + #Checks for :allow_all_actions globally and for individual routes
  8 + #if it is not enabled globally.
  9 + def run_check
  10 + if tracker.routes[:allow_all_actions]
  11 + #Default routes are enabled globally
  12 + warn :warning_type => "Default Routes",
  13 + :message => "All public methods in controllers are available as actions in routes.rb",
  14 + :line => tracker.routes[:allow_all_actions].line,
  15 + :confidence => CONFIDENCE[:high],
  16 + :file => "#{OPTIONS[:app_path]}/config/routes.rb"
  17 + else #Report each controller separately
  18 + tracker.routes.each do |name, actions|
  19 + if actions == :allow_all_actions
  20 + warn :controller => name,
  21 + :warning_type => "Default Routes",
  22 + :message => "Any public method in #{name} can be used as an action.",
  23 + :confidence => CONFIDENCE[:med],
  24 + :file => "#{OPTIONS[:app_path]}/config/routes.rb"
  25 + end
  26 + end
  27 + end
  28 + end
  29 +end
29 lib/checks/check_evaluation.rb
... ... @@ -0,0 +1,29 @@
  1 +require 'checks/base_check'
  2 +
  3 +#This check looks for calls to +eval+, +instance_eval+, etc. which include
  4 +#user input.
  5 +class CheckEvaluation < BaseCheck
  6 + Checks.add self
  7 +
  8 + #Process calls
  9 + def run_check
  10 + calls = tracker.find_call nil, [:eval, :instance_eval, :class_eval, :module_eval]
  11 +
  12 + @templates = tracker.templates