brakeman 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/brakeman +32 -173
- data/lib/brakeman.rb +47 -25
- data/lib/brakeman/call_index.rb +37 -1
- data/lib/brakeman/checks.rb +17 -0
- data/lib/brakeman/checks/base_check.rb +5 -1
- data/lib/brakeman/checks/check_cross_site_scripting.rb +18 -22
- data/lib/brakeman/checks/check_execute.rb +11 -24
- data/lib/brakeman/checks/check_render.rb +15 -26
- data/lib/brakeman/checks/check_sql.rb +48 -3
- data/lib/brakeman/options.rb +204 -0
- data/lib/brakeman/processor.rb +2 -2
- data/lib/brakeman/processors/controller_alias_processor.rb +9 -1
- data/lib/brakeman/processors/lib/find_all_calls.rb +36 -0
- data/lib/brakeman/processors/lib/rails3_route_processor.rb +1 -0
- data/lib/brakeman/processors/model_processor.rb +1 -1
- data/lib/brakeman/report.rb +36 -122
- data/lib/brakeman/rescanner.rb +247 -0
- data/lib/brakeman/scanner.rb +94 -76
- data/lib/brakeman/tracker.rb +103 -2
- data/lib/brakeman/util.rb +106 -0
- data/lib/brakeman/version.rb +1 -1
- data/lib/brakeman/warning.rb +26 -11
- metadata +5 -3
@@ -60,29 +60,18 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
|
|
60
60
|
#
|
61
61
|
# `rm -rf #{params[:file]}`
|
62
62
|
def check_for_backticks tracker
|
63
|
-
tracker.
|
64
|
-
|
65
|
-
@current_method = method_name
|
66
|
-
|
67
|
-
process exp
|
68
|
-
end
|
69
|
-
|
70
|
-
@current_set = nil
|
71
|
-
|
72
|
-
tracker.each_template do |name, template|
|
73
|
-
@current_template = template
|
74
|
-
|
75
|
-
process template[:src]
|
63
|
+
tracker.find_call(:target => nil, :method => :`).each do |result|
|
64
|
+
process_backticks result
|
76
65
|
end
|
77
|
-
|
78
|
-
@current_template = nil
|
79
66
|
end
|
80
67
|
|
81
68
|
#Processes backticks.
|
82
|
-
def
|
83
|
-
return
|
69
|
+
def process_backticks result
|
70
|
+
return if duplicate? result
|
71
|
+
|
72
|
+
add_result result
|
84
73
|
|
85
|
-
|
74
|
+
exp = result[:call]
|
86
75
|
|
87
76
|
if include_user_input? exp
|
88
77
|
confidence = CONFIDENCE[:high]
|
@@ -96,15 +85,13 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
|
|
96
85
|
:code => exp,
|
97
86
|
:confidence => confidence }
|
98
87
|
|
99
|
-
if
|
100
|
-
warning[:template] =
|
88
|
+
if result[:location][0] == :template
|
89
|
+
warning[:template] = result[:location][1]
|
101
90
|
else
|
102
|
-
warning[:class] =
|
103
|
-
warning[:method] =
|
91
|
+
warning[:class] = result[:location][1]
|
92
|
+
warning[:method] = result[:location][2]
|
104
93
|
end
|
105
94
|
|
106
95
|
warn warning
|
107
|
-
|
108
|
-
exp
|
109
96
|
end
|
110
97
|
end
|
@@ -5,24 +5,15 @@ class Brakeman::CheckRender < Brakeman::BaseCheck
|
|
5
5
|
Brakeman::Checks.add self
|
6
6
|
|
7
7
|
def run_check
|
8
|
-
|
9
|
-
|
10
|
-
@current_class = class_name
|
11
|
-
@current_method = method_name
|
12
|
-
process src
|
13
|
-
end
|
14
|
-
|
15
|
-
debug_info "Checking all templates for calls to render()"
|
16
|
-
tracker.each_template do |name, template|
|
17
|
-
@current_template = template
|
18
|
-
process template[:src]
|
8
|
+
tracker.find_call(:target => nil, :method => :render).each do |result|
|
9
|
+
process_render result
|
19
10
|
end
|
20
11
|
end
|
21
12
|
|
22
|
-
def process_render
|
23
|
-
case
|
13
|
+
def process_render result
|
14
|
+
case result[:call][1]
|
24
15
|
when :partial, :template, :action, :file
|
25
|
-
check_for_dynamic_path
|
16
|
+
check_for_dynamic_path result
|
26
17
|
when :inline
|
27
18
|
when :js
|
28
19
|
when :json
|
@@ -30,16 +21,15 @@ class Brakeman::CheckRender < Brakeman::BaseCheck
|
|
30
21
|
when :update
|
31
22
|
when :xml
|
32
23
|
end
|
33
|
-
exp
|
34
24
|
end
|
35
25
|
|
36
26
|
#Check if path to action or file is determined dynamically
|
37
|
-
def check_for_dynamic_path
|
38
|
-
view =
|
27
|
+
def check_for_dynamic_path result
|
28
|
+
view = result[:call][2]
|
39
29
|
|
40
|
-
if sexp? view and view.node_type != :str and view.node_type != :lit and not duplicate?
|
30
|
+
if sexp? view and view.node_type != :str and view.node_type != :lit and not duplicate? result
|
41
31
|
|
42
|
-
add_result
|
32
|
+
add_result result
|
43
33
|
|
44
34
|
if include_user_input? view
|
45
35
|
confidence = CONFIDENCE[:high]
|
@@ -49,16 +39,15 @@ class Brakeman::CheckRender < Brakeman::BaseCheck
|
|
49
39
|
|
50
40
|
warning = { :warning_type => "Dynamic Render Path",
|
51
41
|
:message => "Render path is dynamic",
|
52
|
-
:line =>
|
53
|
-
:code =>
|
42
|
+
:line => result[:call].line,
|
43
|
+
:code => result[:call],
|
54
44
|
:confidence => confidence }
|
55
45
|
|
56
|
-
|
57
|
-
|
58
|
-
warning[:template] = @current_template
|
46
|
+
if result[:location][0] == :template
|
47
|
+
warning[:template] = result[:location][1]
|
59
48
|
else
|
60
|
-
warning[:class] =
|
61
|
-
warning[:method] =
|
49
|
+
warning[:class] = result[:location][1]
|
50
|
+
warning[:method] = result[:location][2]
|
62
51
|
end
|
63
52
|
|
64
53
|
warn warning
|
@@ -31,17 +31,47 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
|
|
31
31
|
debug_info "Finding possible SQL calls using constantized()"
|
32
32
|
calls.concat tracker.find_call(:method => /^(find.*|last|first|all|count|sum|average|minumum|maximum|count_by_sql)$/).select { |result| constantize_call? result }
|
33
33
|
|
34
|
+
debug_info "Finding calls to named_scope or scope"
|
35
|
+
calls.concat find_scope_calls
|
36
|
+
|
34
37
|
debug_info "Processing possible SQL calls"
|
35
38
|
calls.each do |c|
|
36
39
|
process_result c
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
43
|
+
#Find calls to named_scope() or scope() in models
|
44
|
+
def find_scope_calls
|
45
|
+
scope_calls = []
|
46
|
+
|
47
|
+
if version_between? "2.1.0", "3.0.9"
|
48
|
+
tracker.models.each do |name, model|
|
49
|
+
if model[:options][:named_scope]
|
50
|
+
model[:options][:named_scope].each do |args|
|
51
|
+
call = Sexp.new(:call, nil, :named_scope, args).line(args.line)
|
52
|
+
scope_calls << { :call => call, :location => [:class, name ] }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
elsif version_between? "3.1.0", "3.9.9"
|
57
|
+
tracker.models.each do |name, model|
|
58
|
+
if model[:options][:scope]
|
59
|
+
model[:options][:scope].each do |args|
|
60
|
+
call = Sexp.new(:call, nil, :scope, args).line(args.line)
|
61
|
+
scope_calls << { :call => call, :location => [:class, name ] }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
scope_calls
|
68
|
+
end
|
69
|
+
|
40
70
|
#Process result from Tracker#find_call.
|
41
71
|
def process_result result
|
42
72
|
call = result[:call]
|
43
73
|
|
44
|
-
args =
|
74
|
+
args = call[3]
|
45
75
|
|
46
76
|
if call[2] == :find_by_sql or call[2] == :count_by_sql
|
47
77
|
failed = check_arguments args[1]
|
@@ -94,8 +124,8 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
|
|
94
124
|
end
|
95
125
|
when :array
|
96
126
|
return check_arguments(arg[1])
|
97
|
-
when :string_interp
|
98
|
-
return true
|
127
|
+
when :string_interp, :dstr
|
128
|
+
return true if check_string_interp arg
|
99
129
|
when :call
|
100
130
|
return check_call(arg)
|
101
131
|
else
|
@@ -108,11 +138,24 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
|
|
108
138
|
false
|
109
139
|
end
|
110
140
|
|
141
|
+
def check_string_interp arg
|
142
|
+
arg.each do |exp|
|
143
|
+
#For now, don't warn on interpolation of Model.table_name
|
144
|
+
#but check for other 'safe' things in the future
|
145
|
+
if sexp? exp and (exp.node_type == :string_eval or exp.node_type == :evstr)
|
146
|
+
if call? exp[1] and (model_name?(exp[1][1]) or exp[1][1].nil?) and exp[1][2] == :table_name
|
147
|
+
return false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
111
153
|
#Check call for user input and string building
|
112
154
|
def check_call exp
|
113
155
|
target = exp[1]
|
114
156
|
method = exp[2]
|
115
157
|
args = exp[3]
|
158
|
+
|
116
159
|
if sexp? target and
|
117
160
|
(method == :+ or method == :<< or method == :concat) and
|
118
161
|
(string? target or include_user_input? exp)
|
@@ -120,6 +163,8 @@ class Brakeman::CheckSQL < Brakeman::BaseCheck
|
|
120
163
|
true
|
121
164
|
elsif call? target
|
122
165
|
check_call target
|
166
|
+
elsif target == nil and tracker.options[:rails3] and method.to_s.match /^first|last|all|where|order|group|having$/
|
167
|
+
check_arguments args
|
123
168
|
else
|
124
169
|
false
|
125
170
|
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
#Parses command line arguments for Brakeman
|
5
|
+
module Brakeman::Options
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
#Parse argument array
|
10
|
+
def parse args
|
11
|
+
get_options args
|
12
|
+
end
|
13
|
+
|
14
|
+
#Parse arguments and remove them from the array as they are matched
|
15
|
+
def parse! args
|
16
|
+
get_options args, true
|
17
|
+
end
|
18
|
+
|
19
|
+
#Return hash of options and the parser
|
20
|
+
def get_options args, destructive = false
|
21
|
+
options = {}
|
22
|
+
|
23
|
+
parser = OptionParser.new do |opts|
|
24
|
+
opts.banner = "Usage: brakeman [options] rails/root/path"
|
25
|
+
|
26
|
+
opts.on "-n", "--no-threads", "Run checks sequentially" do
|
27
|
+
options[:parallel_checks] = false
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on "--[no-]progress", "Show progress reports" do |progress|
|
31
|
+
options[:report_progress] = progress
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on "-p", "--path PATH", "Specify path to Rails application" do |path|
|
35
|
+
options[:app_path] = File.expand_path path
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on "-q", "--[no-]quiet", "Suppress informational messages" do |quiet|
|
39
|
+
options[:quiet] = quiet
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on( "-z", "--exit-on-warn", "Exit code is non-zero if warnings found") do
|
43
|
+
options[:exit_on_warn] = true
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on "-3", "--rails3", "Force Rails 3 mode" do
|
47
|
+
options[:rails3] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.separator ""
|
51
|
+
opts.separator "Scanning options:"
|
52
|
+
|
53
|
+
opts.on "-a", "--assume-routes", "Assume all controller methods are actions" do
|
54
|
+
options[:assume_all_routes] = true
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on "--ignore-model-output", "Consider model attributes XSS-safe" do
|
58
|
+
options[:ignore_model_output] = true
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on "-e", "--escape-html", "Escape HTML by default" do
|
62
|
+
options[:escape_html] = true
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on "--faster", "Faster, but less accurate scan" do
|
66
|
+
options[:ignore_ifs] = true
|
67
|
+
options[:skip_libs] = true
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on "--no-branching", "Disable flow sensitivity on conditionals" do
|
71
|
+
options[:ignore_ifs] = true
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on "-r", "--report-direct", "Only report direct use of untrusted data" do |option|
|
75
|
+
options[:check_arguments] = !option
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on "-s", "--safe-methods meth1,meth2,etc", Array, "Consider the specified methods safe" do |methods|
|
79
|
+
options[:safe_methods] ||= Set.new
|
80
|
+
options[:safe_methods].merge methods.map {|e| e.to_sym }
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on "--skip-libs", "Skip processing lib directory" do
|
84
|
+
options[:skip_libs] = true
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.on "-t", "--test Check1,Check2,etc", Array, "Only run the specified checks" do |checks|
|
88
|
+
checks.each_with_index do |s, index|
|
89
|
+
if s[0,5] != "Check"
|
90
|
+
checks[index] = "Check" << s
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
options[:run_checks] ||= Set.new
|
95
|
+
options[:run_checks].merge checks
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on "-x", "--except Check1,Check2,etc", Array, "Skip the specified checks" do |skip|
|
99
|
+
skip.each do |s|
|
100
|
+
if s[0,5] != "Check"
|
101
|
+
s = "Check" << s
|
102
|
+
end
|
103
|
+
|
104
|
+
options[:skip_checks] ||= Set.new
|
105
|
+
options[:skip_checks] << s
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
opts.separator ""
|
110
|
+
opts.separator "Output options:"
|
111
|
+
|
112
|
+
opts.on "-d", "--debug", "Lots of output" do
|
113
|
+
options[:debug] = true
|
114
|
+
end
|
115
|
+
|
116
|
+
opts.on "-f",
|
117
|
+
"--format TYPE",
|
118
|
+
[:pdf, :text, :html, :csv, :tabs],
|
119
|
+
"Specify output format. Default is text" do |type|
|
120
|
+
|
121
|
+
type = "s" if type == :text
|
122
|
+
options[:output_format] = ("to_" << type.to_s).to_sym
|
123
|
+
end
|
124
|
+
|
125
|
+
opts.on "--css-file CSSFile", "Specify CSS to use for HTML output" do |file|
|
126
|
+
options[:html_style] = File.expand_path file
|
127
|
+
end
|
128
|
+
|
129
|
+
opts.on "-l", "--[no-]combine-locations", "Combine warning locations (Default)" do |combine|
|
130
|
+
options[:combine_locations] = combine
|
131
|
+
end
|
132
|
+
|
133
|
+
opts.on "-m", "--routes", "Report controller information" do
|
134
|
+
options[:report_routes] = true
|
135
|
+
end
|
136
|
+
|
137
|
+
opts.on "--message-limit LENGTH", "Limit message length in HTML report" do |limit|
|
138
|
+
options[:message_limit] = limit.to_i
|
139
|
+
end
|
140
|
+
|
141
|
+
opts.on "-o", "--output FILE", "Specify file for output. Defaults to stdout" do |file|
|
142
|
+
options[:output_file] = file
|
143
|
+
end
|
144
|
+
|
145
|
+
opts.on "--separate-models", "Warn on each model without attr_accessible" do
|
146
|
+
options[:collapse_mass_assignment] = false
|
147
|
+
end
|
148
|
+
|
149
|
+
opts.on "--summary", "Only output summary of warnings" do
|
150
|
+
options[:summary_only] = true
|
151
|
+
end
|
152
|
+
|
153
|
+
opts.on "-w",
|
154
|
+
"--confidence-level LEVEL",
|
155
|
+
["1", "2", "3"],
|
156
|
+
"Set minimal confidence level (1 - 3)" do |level|
|
157
|
+
|
158
|
+
options[:min_confidence] = 3 - level.to_i
|
159
|
+
end
|
160
|
+
|
161
|
+
opts.separator ""
|
162
|
+
opts.separator "Configuration files:"
|
163
|
+
|
164
|
+
opts.on "-c", "--config-file FILE", "Use specified configuration file" do |file|
|
165
|
+
options[:config_file] = File.expand_path(file)
|
166
|
+
end
|
167
|
+
|
168
|
+
opts.on "-C", "--create-config [FILE]", "Output configuration file based on options" do |file|
|
169
|
+
if file
|
170
|
+
options[:create_config] = file
|
171
|
+
else
|
172
|
+
options[:create_config] = true
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
opts.separator ""
|
177
|
+
|
178
|
+
opts.on "-k", "--checks", "List all available vulnerability checks" do
|
179
|
+
options[:list_checks] = true
|
180
|
+
end
|
181
|
+
|
182
|
+
opts.on "--rake", "Create rake task to run Brakeman" do
|
183
|
+
options[:install_rake_task] = true
|
184
|
+
end
|
185
|
+
|
186
|
+
opts.on "-v", "--version", "Show Brakeman version" do
|
187
|
+
options[:show_version] = true
|
188
|
+
end
|
189
|
+
|
190
|
+
opts.on_tail "-h", "--help", "Display this message" do
|
191
|
+
options[:show_help] = true
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
if destructive
|
196
|
+
parser.parse! args
|
197
|
+
else
|
198
|
+
parser.parse args
|
199
|
+
end
|
200
|
+
|
201
|
+
return options, parser
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
data/lib/brakeman/processor.rb
CHANGED
@@ -40,8 +40,8 @@ module Brakeman
|
|
40
40
|
|
41
41
|
#Process variable aliasing in controller source and save it in the
|
42
42
|
#tracker.
|
43
|
-
def process_controller_alias src
|
44
|
-
ControllerAliasProcessor.new(@tracker).process src
|
43
|
+
def process_controller_alias src, only_method = nil
|
44
|
+
ControllerAliasProcessor.new(@tracker, only_method).process src
|
45
45
|
end
|
46
46
|
|
47
47
|
#Process a model source
|
@@ -6,8 +6,12 @@ require 'brakeman/processors/lib/render_helper'
|
|
6
6
|
class Brakeman::ControllerAliasProcessor < Brakeman::AliasProcessor
|
7
7
|
include Brakeman::RenderHelper
|
8
8
|
|
9
|
-
|
9
|
+
#If only_method is specified, only that method will be processed,
|
10
|
+
#other methods will be skipped.
|
11
|
+
#This is for rescanning just a single action.
|
12
|
+
def initialize tracker, only_method = nil
|
10
13
|
super()
|
14
|
+
@only_method = only_method
|
11
15
|
@tracker = tracker
|
12
16
|
@rendered = false
|
13
17
|
@current_class = @current_module = @current_method = nil
|
@@ -26,6 +30,10 @@ class Brakeman::ControllerAliasProcessor < Brakeman::AliasProcessor
|
|
26
30
|
#Processes a method definition, which may include
|
27
31
|
#processing any rendered templates.
|
28
32
|
def process_methdef exp
|
33
|
+
#Skip if instructed to only process a specific method
|
34
|
+
#(but don't skip if this method was called from elsewhere)
|
35
|
+
return exp if @current_method.nil? and @only_method and @only_method != exp[1]
|
36
|
+
|
29
37
|
is_route = route? exp[1]
|
30
38
|
other_method = @current_method
|
31
39
|
@current_method = exp[1]
|