brakeman 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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