brakeman 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/CHANGES +20 -0
  2. data/README.md +6 -1
  3. data/bin/brakeman +13 -3
  4. data/lib/brakeman.rb +64 -7
  5. data/lib/brakeman/call_index.rb +6 -4
  6. data/lib/brakeman/checks/check_basic_auth.rb +47 -2
  7. data/lib/brakeman/checks/check_cross_site_scripting.rb +50 -12
  8. data/lib/brakeman/checks/check_execute.rb +4 -1
  9. data/lib/brakeman/checks/check_model_attr_accessible.rb +48 -0
  10. data/lib/brakeman/checks/check_sql.rb +101 -154
  11. data/lib/brakeman/options.rb +16 -0
  12. data/lib/brakeman/parsers/rails2_erubis.rb +2 -0
  13. data/lib/brakeman/parsers/rails2_xss_plugin_erubis.rb +2 -0
  14. data/lib/brakeman/parsers/rails3_erubis.rb +2 -0
  15. data/lib/brakeman/processors/alias_processor.rb +19 -4
  16. data/lib/brakeman/processors/controller_alias_processor.rb +2 -3
  17. data/lib/brakeman/processors/gem_processor.rb +5 -4
  18. data/lib/brakeman/processors/lib/find_all_calls.rb +43 -16
  19. data/lib/brakeman/report.rb +39 -640
  20. data/lib/brakeman/report/ignore/config.rb +130 -0
  21. data/lib/brakeman/report/ignore/interactive.rb +311 -0
  22. data/lib/brakeman/report/renderer.rb +2 -0
  23. data/lib/brakeman/report/report_base.rb +279 -0
  24. data/lib/brakeman/report/report_csv.rb +56 -0
  25. data/lib/brakeman/report/report_hash.rb +22 -0
  26. data/lib/brakeman/report/report_html.rb +203 -0
  27. data/lib/brakeman/report/report_json.rb +46 -0
  28. data/lib/brakeman/report/report_table.rb +109 -0
  29. data/lib/brakeman/report/report_tabs.rb +17 -0
  30. data/lib/brakeman/report/templates/ignored_warnings.html.erb +21 -0
  31. data/lib/brakeman/report/templates/overview.html.erb +6 -0
  32. data/lib/brakeman/report/templates/security_warnings.html.erb +1 -1
  33. data/lib/brakeman/scanner.rb +14 -12
  34. data/lib/brakeman/tracker.rb +5 -1
  35. data/lib/brakeman/util.rb +2 -0
  36. data/lib/brakeman/version.rb +1 -1
  37. data/lib/ruby_parser/bm_sexp.rb +12 -1
  38. metadata +179 -90
  39. checksums.yaml +0 -7
data/CHANGES CHANGED
@@ -1,3 +1,23 @@
1
+ # 2.1.0
2
+
3
+ * Support non-native line endings in Gemfile.lock (Paul Deardorff)
4
+ * Support for ignoring warnings
5
+ * Check for dangerous model attributes defined in attr_accessible (Paul Deardorff)
6
+ * Update to ruby_parser 3.2.2
7
+ * Add brakeman-min gemspec
8
+ * Load gem dependencies on-demand
9
+ * Output JSON diff to file if -o option is used
10
+ * Add check for authenticate_or_request_with_http_basic
11
+ * Refactor of SQL injection check code (Bart ten Brinke)
12
+ * Fix detection of duplicate XSS warnings
13
+ * Refactor reports into separate classes
14
+ * Allow use of Slim 2.x (Ian Zabel)
15
+ * Return error exit code when application path is not found
16
+ * Add `--branch-limit` option, limit to 5 by default
17
+ * Add more methods to check for command injection
18
+ * Fix output format detection to be more strict again
19
+ * Allow empty Brakeman configuration file
20
+
1
21
  # 2.0.0
2
22
 
3
23
  * Add `--only-files` option to specify files/paths to scan (Ian Ehlert)
data/README.md CHANGED
@@ -9,7 +9,7 @@ Climate](https://codeclimate.com/github/presidentbeef/brakeman.png)](https://cod
9
9
 
10
10
  Brakeman is a static analysis tool which checks Ruby on Rails applications for security vulnerabilities.
11
11
 
12
- It targets Rails versions 2.x and 3.x.
12
+ It works with Rails 2.x, 3.x, and 4.x.
13
13
 
14
14
  There is also a [plugin available](http://brakemanscanner.org/docs/jenkins/) for Jenkins/Hudson.
15
15
 
@@ -124,6 +124,11 @@ To compare results of a scan with a previous scan, use the JSON output option an
124
124
 
125
125
  This will output JSON with two lists: one of fixed warnings and one of new warnings.
126
126
 
127
+ Brakeman will ignore warnings if configured to do so. By default, it looks for a configuration file in `config/brakeman.ignore`.
128
+ To create and manage this file, use:
129
+
130
+ brakeman -I
131
+
127
132
  # Warning information
128
133
 
129
134
  See WARNING\_TYPES for more information on the warnings reported by this tool.
@@ -59,7 +59,16 @@ end
59
59
  begin
60
60
  if options[:previous_results_json]
61
61
  vulns = Brakeman.compare options.merge(:quiet => options[:quiet])
62
- puts MultiJson.dump(vulns, :pretty => true)
62
+
63
+ if options[:comparison_output_file]
64
+ File.open options[:comparison_output_file], "w" do |f|
65
+ f.puts MultiJson.dump(vulns, :pretty => true)
66
+ end
67
+
68
+ Brakeman.notify "Comparison saved in '#{options[:comparison_output_file]}'"
69
+ else
70
+ puts MultiJson.dump(vulns, :pretty => true)
71
+ end
63
72
 
64
73
  if options[:exit_on_warn] and (vulns[:new].count + vulns[:fixed].count > 0)
65
74
  exit Brakeman::Warnings_Found_Exit_Code
@@ -69,10 +78,11 @@ begin
69
78
  tracker = Brakeman.run options.merge(:print_report => true, :quiet => options[:quiet])
70
79
 
71
80
  #Return error code if --exit-on-warn is used and warnings were found
72
- if options[:exit_on_warn] and not tracker.checks.all_warnings.empty?
81
+ if options[:exit_on_warn] and not tracker.warnings.empty?
73
82
  exit Brakeman::Warnings_Found_Exit_Code
74
83
  end
75
84
  end
76
- rescue Brakeman::Scanner::NoApplication => e
85
+ rescue Brakeman::NoApplication => e
77
86
  $stderr.puts e.message
87
+ exit 1
78
88
  end
@@ -10,6 +10,7 @@ module Brakeman
10
10
 
11
11
  @debug = false
12
12
  @quiet = false
13
+ @loaded_dependencies = []
13
14
 
14
15
  #Run Brakeman scan. Returns Tracker object.
15
16
  #
@@ -91,11 +92,17 @@ module Brakeman
91
92
  #Load configuration file
92
93
  if config = config_file(custom_location)
93
94
  options = YAML.load_file config
94
- options.each { |k, v| options[k] = Set.new v if v.is_a? Array }
95
-
96
- # notify if options[:quiet] and quiet is nil||false
97
- notify "[Notice] Using configuration in #{config}" unless (options[:quiet] || quiet)
98
- options
95
+
96
+ if options
97
+ options.each { |k, v| options[k] = Set.new v if v.is_a? Array }
98
+
99
+ # notify if options[:quiet] and quiet is nil||false
100
+ notify "[Notice] Using configuration in #{config}" unless (options[:quiet] || quiet)
101
+ options
102
+ else
103
+ notify "[Notice] Empty configuration file: #{config}" unless quiet
104
+ {}
105
+ end
99
106
  else
100
107
  {}
101
108
  end
@@ -138,7 +145,12 @@ module Brakeman
138
145
  elsif options[:output_files]
139
146
  get_formats_from_output_files options[:output_files]
140
147
  else
141
- return [:to_s]
148
+ begin
149
+ require 'terminal-table'
150
+ return [:to_s]
151
+ rescue LoadError
152
+ return [:to_json]
153
+ end
142
154
  end
143
155
  end
144
156
 
@@ -278,8 +290,11 @@ module Brakeman
278
290
  else
279
291
  notify "Runnning checks..."
280
292
  end
293
+
281
294
  tracker.run_checks
282
295
 
296
+ self.filter_warnings tracker, options
297
+
283
298
  if options[:output_files]
284
299
  notify "Generating report..."
285
300
 
@@ -296,7 +311,7 @@ module Brakeman
296
311
  def self.write_report_to_files tracker, output_files
297
312
  output_files.each_with_index do |output_file, idx|
298
313
  File.open output_file, "w" do |f|
299
- f.write tracker.report.format(output_file)
314
+ f.write tracker.report.format(tracker.options[:output_formats][idx])
300
315
  end
301
316
  notify "Report saved in '#{output_file}'"
302
317
  end
@@ -360,6 +375,48 @@ module Brakeman
360
375
  Brakeman::Differ.new(new_results, previous_results).diff
361
376
  end
362
377
 
378
+ def self.load_dependency name
379
+ return if @loaded_dependencies.include? name
380
+
381
+ begin
382
+ require name
383
+ rescue LoadError => e
384
+ $stderr.puts e.message
385
+ $stderr.puts "Please install the appropriate dependency."
386
+ exit! -1
387
+ end
388
+ end
389
+
390
+ def self.filter_warnings tracker, options
391
+ require 'brakeman/report/ignore/config'
392
+
393
+ app_tree = Brakeman::AppTree.from_options(options)
394
+
395
+ if options[:ignore_file]
396
+ file = options[:ignore_file]
397
+ elsif app_tree.exists? "config/brakeman.ignore"
398
+ file = app_tree.expand_path("config/brakeman.ignore")
399
+ elsif not options[:interactive_ignore]
400
+ return
401
+ end
402
+
403
+ notify "Filtering warnings..."
404
+
405
+ if options[:interactive_ignore]
406
+ require 'brakeman/report/ignore/interactive'
407
+ config = InteractiveIgnorer.new(file, tracker.warnings).start
408
+ else
409
+ notify "[Notice] Using '#{file}' to filter warnings"
410
+ config = IgnoreConfig.new(file, tracker.warnings)
411
+ config.read_from_file
412
+ config.filter_ignored
413
+ end
414
+
415
+ tracker.ignored_filter = config
416
+ end
417
+
418
+ class DependencyError < RuntimeError; end
363
419
  class RakeInstallError < RuntimeError; end
364
420
  class NoBrakemanError < RuntimeError; end
421
+ class NoApplication < RuntimeError; end
365
422
  end
@@ -5,8 +5,8 @@ class Brakeman::CallIndex
5
5
 
6
6
  #Initialize index with calls from FindAllCalls
7
7
  def initialize calls
8
- @calls_by_method = Hash.new { |h,k| h[k] = [] }
9
- @calls_by_target = Hash.new { |h,k| h[k] = [] }
8
+ @calls_by_method = Hash.new
9
+ @calls_by_target = Hash.new
10
10
 
11
11
  index_calls calls
12
12
  end
@@ -95,9 +95,11 @@ class Brakeman::CallIndex
95
95
 
96
96
  def index_calls calls
97
97
  calls.each do |call|
98
+ @calls_by_method[call[:method]] ||= []
98
99
  @calls_by_method[call[:method]] << call
99
100
 
100
101
  unless call[:target].is_a? Sexp
102
+ @calls_by_target[call[:target]] ||= []
101
103
  @calls_by_target[call[:target]] << call
102
104
  end
103
105
  end
@@ -138,7 +140,7 @@ class Brakeman::CallIndex
138
140
  if method.is_a? Array
139
141
  calls_by_methods method
140
142
  else
141
- @calls_by_method[method.to_sym]
143
+ @calls_by_method[method.to_sym] || []
142
144
  end
143
145
  end
144
146
 
@@ -154,7 +156,7 @@ class Brakeman::CallIndex
154
156
  end
155
157
 
156
158
  def calls_with_no_target
157
- @calls_by_target[nil]
159
+ @calls_by_target[nil] || []
158
160
  end
159
161
 
160
162
  def filter calls, key, value
@@ -12,6 +12,11 @@ class Brakeman::CheckBasicAuth < Brakeman::BaseCheck
12
12
  def run_check
13
13
  return if version_between? "0.0.0", "3.0.99"
14
14
 
15
+ check_basic_auth_filter
16
+ check_basic_auth_request
17
+ end
18
+
19
+ def check_basic_auth_filter
15
20
  controllers = tracker.controllers.select do |name, c|
16
21
  c[:options][:http_basic_authenticate_with]
17
22
  end
@@ -21,10 +26,10 @@ class Brakeman::CheckBasicAuth < Brakeman::BaseCheck
21
26
 
22
27
  if pass = get_password(call) and string? pass
23
28
  warn :controller => name,
24
- :warning_type => "Basic Auth",
29
+ :warning_type => "Basic Auth",
25
30
  :warning_code => :basic_auth_password,
26
31
  :message => "Basic authentication password stored in source code",
27
- :code => call,
32
+ :code => call,
28
33
  :confidence => 0,
29
34
  :file => controller[:file]
30
35
 
@@ -34,6 +39,46 @@ class Brakeman::CheckBasicAuth < Brakeman::BaseCheck
34
39
  end
35
40
  end
36
41
 
42
+ # Look for
43
+ # authenticate_or_request_with_http_basic do |username, password|
44
+ # username == "foo" && password == "bar"
45
+ # end
46
+ def check_basic_auth_request
47
+ tracker.find_call(:target => nil, :method => :authenticate_or_request_with_http_basic).each do |result|
48
+ if include_password_literal? result
49
+ warn :result => result,
50
+ :code => @include_password,
51
+ :warning_type => "Basic Auth",
52
+ :warning_code => :basic_auth_password,
53
+ :message => "Basic authentication password stored in source code",
54
+ :confidence => 0
55
+ end
56
+ end
57
+ end
58
+
59
+ # Check if the block of a result contains a comparison of password to string
60
+ def include_password_literal? result
61
+ @password_var = result[:block_args].last
62
+ @include_password = false
63
+ process result[:block]
64
+ @include_password
65
+ end
66
+
67
+ # Looks for :== calls on password var
68
+ def process_call exp
69
+ target = exp.target
70
+
71
+ if node_type?(target, :lvar) and
72
+ target.value == @password_var and
73
+ exp.method == :== and
74
+ string? exp.first_arg
75
+
76
+ @include_password = exp
77
+ end
78
+
79
+ exp
80
+ end
81
+
37
82
  def get_password call
38
83
  arg = call.first_arg
39
84
 
@@ -92,7 +92,7 @@ class Brakeman::CheckCrossSiteScripting < Brakeman::BaseCheck
92
92
  end
93
93
 
94
94
  def check_for_immediate_xss exp
95
- return if duplicate? exp
95
+ return :duplicate if duplicate? exp
96
96
 
97
97
  if exp.node_type == :output
98
98
  out = exp.value
@@ -120,7 +120,7 @@ class Brakeman::CheckCrossSiteScripting < Brakeman::BaseCheck
120
120
  end
121
121
 
122
122
  unless IGNORE_MODEL_METHODS.include? method
123
- add_result out
123
+ add_result exp
124
124
 
125
125
  if MODEL_METHODS.include? method or method.to_s =~ /^find_by/
126
126
  confidence = CONFIDENCE[:high]
@@ -229,16 +229,7 @@ class Brakeman::CheckCrossSiteScripting < Brakeman::BaseCheck
229
229
  method = exp.method
230
230
 
231
231
  #Ignore safe items
232
- if (target.nil? and (@ignore_methods.include? method or method.to_s =~ IGNORE_LIKE)) or
233
- (@matched and @matched.type == :model and IGNORE_MODEL_METHODS.include? method) or
234
- (target == HAML_HELPERS and method == :html_escape) or
235
- ((target == URI or target == CGI) and method == :escape) or
236
- (target == XML_HELPER and method == :escape_xml) or
237
- (target == FORM_BUILDER and @ignore_methods.include? method) or
238
- (target and @safe_input_attributes.include? method) or
239
- (method.to_s[-1,1] == "?")
240
-
241
- #exp[0] = :ignore #should not be necessary
232
+ if ignore_call? target, method
242
233
  @matched = false
243
234
  elsif sexp? target and model_name? target[1] #TODO: use method call?
244
235
  @matched = Match.new(:model, exp)
@@ -293,4 +284,51 @@ class Brakeman::CheckCrossSiteScripting < Brakeman::BaseCheck
293
284
  def raw_call? exp
294
285
  exp.value.node_type == :call and exp.value.method == :raw
295
286
  end
287
+
288
+ def ignore_call? target, method
289
+ ignored_method?(target, method) or
290
+ safe_input_attribute?(target, method) or
291
+ ignored_model_method?(method) or
292
+ form_builder_method?(target, method) or
293
+ haml_escaped?(target, method) or
294
+ boolean_method?(method) or
295
+ cgi_escaped?(target, method) or
296
+ xml_escaped?(target, method)
297
+ end
298
+
299
+ def ignored_model_method? method
300
+ @matched and
301
+ @matched.type == :model and
302
+ IGNORE_MODEL_METHODS.include? method
303
+ end
304
+
305
+ def ignored_method? target, method
306
+ target.nil? and
307
+ (@ignore_methods.include? method or method.to_s =~ IGNORE_LIKE)
308
+ end
309
+
310
+ def cgi_escaped? target, method
311
+ method == :escape and
312
+ (target == URI or target == CGI)
313
+ end
314
+
315
+ def haml_escaped? target, method
316
+ method == :html_escape and target == HAML_HELPERS
317
+ end
318
+
319
+ def xml_escaped? target, method
320
+ method == :escape_xml and target == XML_HELPER
321
+ end
322
+
323
+ def form_builder_method? target, method
324
+ target == FORM_BUILDER and @ignore_methods.include? method
325
+ end
326
+
327
+ def safe_input_attribute? target, method
328
+ target and @safe_input_attributes.include? method
329
+ end
330
+
331
+ def boolean_method? method
332
+ method.to_s.end_with? "?"
333
+ end
296
334
  end
@@ -19,7 +19,10 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
19
19
  check_for_backticks tracker
20
20
 
21
21
  Brakeman.debug "Finding other system calls"
22
- calls = tracker.find_call :targets => [:IO, :Open3, :Kernel, nil], :methods => [:exec, :popen, :popen3, :syscall, :system]
22
+ calls = tracker.find_call :targets => [:IO, :Open3, :Kernel, :'POSIX::Spawn', :Process, nil],
23
+ :methods => [:capture2, :capture2e, :capture3, :exec, :pipeline, :pipeline_r,
24
+ :pipeline_rw, :pipeline_start, :pipeline_w, :popen, :popen2, :popen2e,
25
+ :popen3, :spawn, :syscall, :system]
23
26
 
24
27
  Brakeman.debug "Processing system calls"
25
28
  calls.each do |result|
@@ -0,0 +1,48 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ # Author: Paul Deardorff (themetric)
4
+ # Checks models to see if important foreign keys
5
+ # or attributes are exposed as attr_accessible when
6
+ # they probably shouldn't be.
7
+
8
+ class Brakeman::CheckModelAttrAccessible < Brakeman::BaseCheck
9
+ Brakeman::Checks.add self
10
+
11
+ @description = "Reports models which have dangerous attributes defined under the attr_accessible whitelist."
12
+
13
+ SUSP_ATTRS = {
14
+ /admin/ => CONFIDENCE[:high], # Very dangerous unless some Rails authorization used
15
+ /role/ => CONFIDENCE[:med],
16
+ /banned/ => CONFIDENCE[:med],
17
+ :account_id => CONFIDENCE[:high],
18
+ /\S*_id(s?)\z/ => CONFIDENCE[:low] # All other foreign keys have weak/low confidence
19
+ }
20
+
21
+ def run_check
22
+ check_models do |name, model|
23
+ accessible_attrs = model[:attr_accessible]
24
+ accessible_attrs.each do |attribute|
25
+ SUSP_ATTRS.each do |susp_attr, confidence|
26
+ if susp_attr.is_a?(Regexp) and susp_attr =~ attribute.to_s or susp_attr == attribute
27
+ warn :model => name,
28
+ :file => model[:file],
29
+ :warning_type => "Mass Assignment",
30
+ :warning_code => :mass_assign_call,
31
+ :message => "Potentially dangerous attribute #{attribute} available for mass assignment.",
32
+ :confidence => confidence
33
+ break # Prevent from matching single attr multiple times
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def check_models
41
+ tracker.models.each do |name, model|
42
+ if !model[:attr_accessible].nil?
43
+ yield name, model
44
+ end
45
+ end
46
+ end
47
+
48
+ end