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.
- 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
|