brakeman 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +20 -0
- data/README.md +6 -1
- data/bin/brakeman +13 -3
- data/lib/brakeman.rb +64 -7
- data/lib/brakeman/call_index.rb +6 -4
- data/lib/brakeman/checks/check_basic_auth.rb +47 -2
- data/lib/brakeman/checks/check_cross_site_scripting.rb +50 -12
- data/lib/brakeman/checks/check_execute.rb +4 -1
- data/lib/brakeman/checks/check_model_attr_accessible.rb +48 -0
- data/lib/brakeman/checks/check_sql.rb +101 -154
- data/lib/brakeman/options.rb +16 -0
- data/lib/brakeman/parsers/rails2_erubis.rb +2 -0
- data/lib/brakeman/parsers/rails2_xss_plugin_erubis.rb +2 -0
- data/lib/brakeman/parsers/rails3_erubis.rb +2 -0
- data/lib/brakeman/processors/alias_processor.rb +19 -4
- data/lib/brakeman/processors/controller_alias_processor.rb +2 -3
- data/lib/brakeman/processors/gem_processor.rb +5 -4
- data/lib/brakeman/processors/lib/find_all_calls.rb +43 -16
- data/lib/brakeman/report.rb +39 -640
- data/lib/brakeman/report/ignore/config.rb +130 -0
- data/lib/brakeman/report/ignore/interactive.rb +311 -0
- data/lib/brakeman/report/renderer.rb +2 -0
- data/lib/brakeman/report/report_base.rb +279 -0
- data/lib/brakeman/report/report_csv.rb +56 -0
- data/lib/brakeman/report/report_hash.rb +22 -0
- data/lib/brakeman/report/report_html.rb +203 -0
- data/lib/brakeman/report/report_json.rb +46 -0
- data/lib/brakeman/report/report_table.rb +109 -0
- data/lib/brakeman/report/report_tabs.rb +17 -0
- data/lib/brakeman/report/templates/ignored_warnings.html.erb +21 -0
- data/lib/brakeman/report/templates/overview.html.erb +6 -0
- data/lib/brakeman/report/templates/security_warnings.html.erb +1 -1
- data/lib/brakeman/scanner.rb +14 -12
- data/lib/brakeman/tracker.rb +5 -1
- data/lib/brakeman/util.rb +2 -0
- data/lib/brakeman/version.rb +1 -1
- data/lib/ruby_parser/bm_sexp.rb +12 -1
- metadata +179 -90
- 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
|
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.
|
data/bin/brakeman
CHANGED
@@ -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
|
-
|
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.
|
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::
|
85
|
+
rescue Brakeman::NoApplication => e
|
77
86
|
$stderr.puts e.message
|
87
|
+
exit 1
|
78
88
|
end
|
data/lib/brakeman.rb
CHANGED
@@ -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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
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(
|
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
|
data/lib/brakeman/call_index.rb
CHANGED
@@ -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
|
9
|
-
@calls_by_target = Hash.new
|
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
|
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
|
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,
|
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
|