brakeman 0.0.2

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 (47) hide show
  1. data/FEATURES +16 -0
  2. data/README.md +112 -0
  3. data/WARNING_TYPES +69 -0
  4. data/bin/brakeman +266 -0
  5. data/lib/checks.rb +67 -0
  6. data/lib/checks/base_check.rb +338 -0
  7. data/lib/checks/check_cross_site_scripting.rb +216 -0
  8. data/lib/checks/check_default_routes.rb +29 -0
  9. data/lib/checks/check_evaluation.rb +29 -0
  10. data/lib/checks/check_execute.rb +110 -0
  11. data/lib/checks/check_file_access.rb +46 -0
  12. data/lib/checks/check_forgery_setting.rb +25 -0
  13. data/lib/checks/check_mass_assignment.rb +72 -0
  14. data/lib/checks/check_model_attributes.rb +36 -0
  15. data/lib/checks/check_redirect.rb +98 -0
  16. data/lib/checks/check_render.rb +65 -0
  17. data/lib/checks/check_send_file.rb +15 -0
  18. data/lib/checks/check_session_settings.rb +36 -0
  19. data/lib/checks/check_sql.rb +124 -0
  20. data/lib/checks/check_validation_regex.rb +60 -0
  21. data/lib/format/style.css +105 -0
  22. data/lib/processor.rb +83 -0
  23. data/lib/processors/alias_processor.rb +384 -0
  24. data/lib/processors/base_processor.rb +235 -0
  25. data/lib/processors/config_processor.rb +146 -0
  26. data/lib/processors/controller_alias_processor.rb +222 -0
  27. data/lib/processors/controller_processor.rb +175 -0
  28. data/lib/processors/erb_template_processor.rb +84 -0
  29. data/lib/processors/erubis_template_processor.rb +62 -0
  30. data/lib/processors/haml_template_processor.rb +115 -0
  31. data/lib/processors/lib/find_call.rb +176 -0
  32. data/lib/processors/lib/find_model_call.rb +39 -0
  33. data/lib/processors/lib/processor_helper.rb +36 -0
  34. data/lib/processors/lib/render_helper.rb +118 -0
  35. data/lib/processors/library_processor.rb +117 -0
  36. data/lib/processors/model_processor.rb +125 -0
  37. data/lib/processors/output_processor.rb +204 -0
  38. data/lib/processors/params_processor.rb +77 -0
  39. data/lib/processors/route_processor.rb +338 -0
  40. data/lib/processors/template_alias_processor.rb +86 -0
  41. data/lib/processors/template_processor.rb +55 -0
  42. data/lib/report.rb +628 -0
  43. data/lib/scanner.rb +232 -0
  44. data/lib/tracker.rb +144 -0
  45. data/lib/util.rb +141 -0
  46. data/lib/warning.rb +97 -0
  47. metadata +191 -0
@@ -0,0 +1,67 @@
1
+ #Collects up results from running different checks.
2
+ #
3
+ #Checks can be added with +Check.add(check_class)+
4
+ #
5
+ #All .rb files in checks/ will be loaded.
6
+ class Checks
7
+ @checks = []
8
+
9
+ attr_reader :warnings, :controller_warnings, :model_warnings, :template_warnings, :checks_run
10
+
11
+ #Add a check. This will call +_klass_.new+ when running tests
12
+ def self.add klass
13
+ @checks << klass
14
+ end
15
+
16
+ #No need to use this directly.
17
+ def initialize
18
+ @warnings = []
19
+ @template_warnings = []
20
+ @model_warnings = []
21
+ @controller_warnings = []
22
+ @checks_run = []
23
+ end
24
+
25
+ #Add Warning to list of warnings to report.
26
+ #Warnings are split into four different arrays
27
+ #for template, controller, model, and generic warnings.
28
+ def add_warning warning
29
+ case warning.warning_set
30
+ when :template
31
+ @template_warnings << warning
32
+ when :warning
33
+ @warnings << warning
34
+ when :controller
35
+ @controller_warnings << warning
36
+ when :model
37
+ @model_warnings << warning
38
+ else
39
+ raise "Unknown warning: #{warning.warning_set}"
40
+ end
41
+ end
42
+
43
+ #Run all the checks on the given Tracker.
44
+ #Returns a new instance of Checks with the results.
45
+ def self.run_checks tracker
46
+ checks = self.new
47
+ @checks.each do |c|
48
+ #Run or don't run check based on options
49
+ unless OPTIONS[:skip_checks].include? c.to_s or
50
+ (OPTIONS[:run_checks] and not OPTIONS[:run_checks].include? c.to_s)
51
+
52
+ warn " - #{c}"
53
+ c.new(checks, tracker).run_check
54
+
55
+ #Maintain list of which checks were run
56
+ #mainly for reporting purposes
57
+ checks.checks_run << c.to_s[5..-1]
58
+ end
59
+ end
60
+ checks
61
+ end
62
+ end
63
+
64
+ #Load all files in checks/ directory
65
+ Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/checks/*.rb").sort.each do |f|
66
+ require f.match(/(checks\/.*)\.rb$/)[0]
67
+ end
@@ -0,0 +1,338 @@
1
+ require 'rubygems'
2
+ require 'sexp_processor'
3
+ require 'processors/output_processor'
4
+ require 'warning'
5
+ require 'util'
6
+
7
+ #Basis of vulnerability checks.
8
+ class BaseCheck < SexpProcessor
9
+ include ProcessorHelper
10
+ include Util
11
+ attr_reader :checks, :tracker
12
+
13
+ CONFIDENCE = { :high => 0, :med => 1, :low => 2 }
14
+
15
+ #Initialize Check with Checks.
16
+ def initialize checks, tracker
17
+ super()
18
+ @results = [] #only to check for duplicates
19
+ @checks = checks
20
+ @tracker = tracker
21
+ @string_interp = false
22
+ @current_template = @current_module = @current_class = @current_method = nil
23
+ self.strict = false
24
+ self.auto_shift_type = false
25
+ self.require_empty = false
26
+ self.default_method = :process_default
27
+ self.warn_on_default = false
28
+ end
29
+
30
+ #Add result to result list, which is used to check for duplicates
31
+ def add_result result, location = nil
32
+ location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
33
+ location = location[:name] if location.is_a? Hash
34
+ location = location.to_sym
35
+
36
+ @results << [result.line, location, result]
37
+ end
38
+
39
+ #Default Sexp processing. Iterates over each value in the Sexp
40
+ #and processes them if they are also Sexps.
41
+ def process_default exp
42
+ type = exp.shift
43
+ exp.each_with_index do |e, i|
44
+ if sexp? e
45
+ process e
46
+ else
47
+ e
48
+ end
49
+ end
50
+
51
+ exp.unshift type
52
+ end
53
+
54
+ #Process calls and check if they include user input
55
+ def process_call exp
56
+ process exp[1] if sexp? exp[1]
57
+ process exp[3]
58
+
59
+ if ALL_PARAMETERS.include? exp[1] or ALL_PARAMETERS.include? exp or params? exp[1]
60
+ @has_user_input = :params
61
+ elsif exp[1] == COOKIES or exp == COOKIES or cookies? exp[1]
62
+ @has_user_input = :cookies
63
+ elsif sexp? exp[1] and model_name? exp[1][1]
64
+ @has_user_input = :model
65
+ end
66
+
67
+ exp
68
+ end
69
+
70
+ #Note that params are included in current expression
71
+ def process_params exp
72
+ @has_user_input = :params
73
+ exp
74
+ end
75
+
76
+ #Note that cookies are included in current expression
77
+ def process_cookies exp
78
+ @has_user_input = :cookies
79
+ exp
80
+ end
81
+
82
+ private
83
+
84
+ #Report a warning
85
+ def warn options
86
+ @checks.add_warning Warning.new(options.merge({ :check => self.class.to_s }))
87
+ end
88
+
89
+ #Run _exp_ through OutputProcessor to get a nice String.
90
+ def format_output exp
91
+ OutputProcessor.new.format(exp).gsub(/\r|\n/, "")
92
+ end
93
+
94
+ #Checks if the model inherits from parent,
95
+ def parent? tracker, model, parent
96
+ if model == nil
97
+ false
98
+ elsif model[:parent] == parent
99
+ true
100
+ elsif model[:parent]
101
+ parent? tracker, tracker.models[model[:parent]], parent
102
+ else
103
+ false
104
+ end
105
+ end
106
+
107
+ #Checks if mass assignment is disabled globally in an initializer.
108
+ def mass_assign_disabled? tracker
109
+ matches = tracker.check_initializers(:"ActiveRecord::Base", :send)
110
+ if matches.empty?
111
+ false
112
+ else
113
+ matches.each do |result|
114
+ if result[3][3] == Sexp.new(:arg_list, Sexp.new(:lit, :attr_accessible), Sexp.new(:nil))
115
+ return true
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ #This is to avoid reporting duplicates. Checks if the result has been
122
+ #reported already from the same line number.
123
+ def duplicate? result, location = nil
124
+ line = result.line
125
+ location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
126
+
127
+ location = location[:name] if location.is_a? Hash
128
+ location = location.to_sym
129
+ @results.each do |r|
130
+ if r[0] == line and r[1] == location
131
+ if OPTIONS[:combine_locations]
132
+ return true
133
+ elsif r[2] == result
134
+ return true
135
+ end
136
+ end
137
+ end
138
+
139
+ false
140
+ end
141
+
142
+ #Ignores ignores
143
+ def process_ignore exp
144
+ exp
145
+ end
146
+
147
+ #Does not actually process string interpolation, but notes that it occurred.
148
+ def process_string_interp exp
149
+ @string_interp = true
150
+ exp
151
+ end
152
+
153
+ #Checks if an expression contains string interpolation.
154
+ def include_interp? exp
155
+ @string_interp = false
156
+ process exp
157
+ @string_interp
158
+ end
159
+
160
+ #Checks if _exp_ includes parameters or cookies, but this only works
161
+ #with the base process_default.
162
+ def include_user_input? exp
163
+ @has_user_input = false
164
+ process exp
165
+ @has_user_input
166
+ end
167
+
168
+ #This is used to check for user input being used directly.
169
+ #
170
+ #Returns false if none is found, otherwise it returns an array
171
+ #where the first element is the type of user input
172
+ #(either :params or :cookies) and the second element is the matching
173
+ #expression
174
+ def has_immediate_user_input? exp
175
+ if params? exp
176
+ return :params, exp
177
+ elsif cookies? exp
178
+ return :cookies, exp
179
+ elsif call? exp
180
+ if sexp? exp[1]
181
+ if ALL_PARAMETERS.include? exp[1] or params? exp[1]
182
+ return :params, exp
183
+ elsif exp[1] == COOKIES
184
+ return :cookies, exp
185
+ else
186
+ false
187
+ end
188
+ else
189
+ false
190
+ end
191
+ elsif sexp? exp
192
+ case exp.node_type
193
+ when :string_interp
194
+ exp.each do |e|
195
+ if sexp? e
196
+ type, match = has_immediate_user_input?(e)
197
+ if type
198
+ return type, match
199
+ end
200
+ end
201
+ end
202
+ false
203
+ when :string_eval
204
+ if sexp? exp[1]
205
+ if exp[1].node_type == :rlist
206
+ exp[1].each do |e|
207
+ if sexp? e
208
+ type, match = has_immediate_user_input?(e)
209
+ if type
210
+ return type, match
211
+ end
212
+ end
213
+ end
214
+ false
215
+ else
216
+ has_immediate_user_input? exp[1]
217
+ end
218
+ end
219
+ when :format
220
+ has_immediate_user_input? exp[1]
221
+ when :if
222
+ (sexp? exp[2] and has_immediate_user_input? exp[2]) or
223
+ (sexp? exp[3] and has_immediate_user_input? exp[3])
224
+ else
225
+ false
226
+ end
227
+ end
228
+ end
229
+
230
+ #Checks for a model attribute at the top level of the
231
+ #expression.
232
+ def has_immediate_model? exp, out = nil
233
+ out = exp if out.nil?
234
+
235
+ if sexp? exp and exp.node_type == :output
236
+ exp = exp[1]
237
+ end
238
+
239
+ if call? exp
240
+ target = exp[1]
241
+ method = exp[2]
242
+
243
+ if call? target and not method.to_s[-1,1] == "?"
244
+ has_immediate_model? target, out
245
+ elsif model_name? target
246
+ exp
247
+ else
248
+ false
249
+ end
250
+ elsif sexp? exp
251
+ case exp.node_type
252
+ when :string_interp
253
+ exp.each do |e|
254
+ if sexp? e and match = has_immediate_model?(e, out)
255
+ return match
256
+ end
257
+ end
258
+ false
259
+ when :string_eval
260
+ if sexp? exp[1]
261
+ if exp[1].node_type == :rlist
262
+ exp[1].each do |e|
263
+ if sexp? e and match = has_immediate_model?(e, out)
264
+ return match
265
+ end
266
+ end
267
+ false
268
+ else
269
+ has_immediate_model? exp[1], out
270
+ end
271
+ end
272
+ when :format
273
+ has_immediate_model? exp[1], out
274
+ when :if
275
+ ((sexp? exp[2] and has_immediate_model? exp[2], out) or
276
+ (sexp? exp[3] and has_immediate_model? exp[3], out))
277
+ else
278
+ false
279
+ end
280
+ end
281
+ end
282
+
283
+ #Checks if +exp+ is a model name.
284
+ #
285
+ #Prior to using this method, either @tracker must be set to
286
+ #the current tracker, or else @models should contain an array of the model
287
+ #names, which is available via tracker.models.keys
288
+ def model_name? exp
289
+ @models ||= @tracker.models.keys
290
+
291
+ if exp.is_a? Symbol
292
+ @models.include? exp
293
+ elsif sexp? exp
294
+ klass = nil
295
+ begin
296
+ klass = class_name exp
297
+ rescue StandardError
298
+ end
299
+
300
+ klass and @models.include? klass
301
+ else
302
+ false
303
+ end
304
+ end
305
+
306
+ #Finds entire method call chain where +target+ is a target in the chain
307
+ def find_chain exp, target
308
+ return unless sexp? exp
309
+
310
+ case exp.node_type
311
+ when :output, :format
312
+ find_chain exp[1], target
313
+ when :call
314
+ if exp == target or include_target? exp, target
315
+ return exp
316
+ end
317
+ else
318
+ exp.each do |e|
319
+ if sexp? e
320
+ res = find_chain e, target
321
+ return res if res
322
+ end
323
+ end
324
+ nil
325
+ end
326
+ end
327
+
328
+ #Returns true if +target+ is in +exp+
329
+ def include_target? exp, target
330
+ return false unless call? exp
331
+
332
+ exp.each do |e|
333
+ return true if e == target or include_target? e, target
334
+ end
335
+
336
+ false
337
+ end
338
+ end
@@ -0,0 +1,216 @@
1
+ require 'checks/base_check'
2
+ require 'processors/lib/find_call'
3
+ require 'processors/lib/processor_helper'
4
+ require 'util'
5
+ require 'set'
6
+
7
+ #This check looks for unescaped output in templates which contains
8
+ #parameters or model attributes.
9
+ #
10
+ #For example:
11
+ #
12
+ # <%= User.find(:id).name %>
13
+ # <%= params[:id] %>
14
+ class CheckCrossSiteScripting < BaseCheck
15
+ Checks.add self
16
+
17
+ #Ignore these methods and their arguments.
18
+ #It is assumed they will take care of escaping their output.
19
+ IGNORE_METHODS = Set.new([:h, :escapeHTML, :link_to, :text_field_tag, :hidden_field_tag,
20
+ :image_tag, :select, :submit_tag, :hidden_field, :url_encode,
21
+ :radio_button, :will_paginate, :button_to, :url_for, :mail_to,
22
+ :fields_for, :label, :text_area, :text_field, :hidden_field, :check_box,
23
+ :field_field])
24
+
25
+ IGNORE_MODEL_METHODS = Set.new([:average, :count, :maximum, :minimum, :sum])
26
+
27
+ MODEL_METHODS = Set.new([:all, :find, :first, :last, :new])
28
+
29
+ IGNORE_LIKE = /^link_to_|_path|_tag|_url$/
30
+
31
+ HAML_HELPERS = Sexp.new(:colon2, Sexp.new(:const, :Haml), :Helpers)
32
+
33
+ URI = Sexp.new(:const, :URI)
34
+
35
+ CGI = Sexp.new(:const, :CGI)
36
+
37
+ FORM_BUILDER = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new, Sexp.new(:arglist))
38
+
39
+ #Run check
40
+ def run_check
41
+ IGNORE_METHODS.merge OPTIONS[:safe_methods]
42
+ @models = tracker.models.keys
43
+ @inspect_arguments = OPTIONS[:check_arguments]
44
+
45
+ tracker.each_template do |name, template|
46
+ @current_template = template
47
+
48
+ template[:outputs].each do |out|
49
+ type, match = has_immediate_user_input?(out[1])
50
+ if type
51
+ unless duplicate? out
52
+ add_result out
53
+ case type
54
+ when :params
55
+
56
+ warn :template => @current_template,
57
+ :warning_type => "Cross Site Scripting",
58
+ :message => "Unescaped parameter value",
59
+ :line => match.line,
60
+ :code => match,
61
+ :confidence => CONFIDENCE[:high]
62
+
63
+ when :cookies
64
+
65
+ warn :template => @current_template,
66
+ :warning_type => "Cross Site Scripting",
67
+ :message => "Unescaped cookie value",
68
+ :line => match.line,
69
+ :code => match,
70
+ :confidence => CONFIDENCE[:high]
71
+ end
72
+ end
73
+ elsif not OPTIONS[:ignore_model_output] and match = has_immediate_model?(out[1])
74
+ method = match[2]
75
+
76
+ unless duplicate? out or IGNORE_MODEL_METHODS.include? method
77
+ add_result out
78
+
79
+ if MODEL_METHODS.include? method or method.to_s =~ /^find_by/
80
+ confidence = CONFIDENCE[:high]
81
+ else
82
+ confidence = CONFIDENCE[:med]
83
+ end
84
+
85
+ code = find_chain out, match
86
+ warn :template => @current_template,
87
+ :warning_type => "Cross Site Scripting",
88
+ :message => "Unescaped model attribute",
89
+ :line => code.line,
90
+ :code => code,
91
+ :confidence => confidence
92
+ end
93
+
94
+ else
95
+ @matched = false
96
+ @mark = false
97
+ process out
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ #Process an output Sexp
104
+ def process_output exp
105
+ process exp[1]
106
+ end
107
+
108
+ #Check a call for user input
109
+ #
110
+ #
111
+ #Since we want to report an entire call and not just part of one, use @mark
112
+ #to mark when a call is started. Any dangerous values inside will then
113
+ #report the entire call chain.
114
+ def process_call exp
115
+ if @mark
116
+ actually_process_call exp
117
+ else
118
+ @mark = true
119
+ actually_process_call exp
120
+ message = nil
121
+
122
+ if @matched == :model and not OPTIONS[:ignore_model_output]
123
+ message = "Unescaped model attribute"
124
+ elsif @matched == :params
125
+ message = "Unescaped parameter value"
126
+ end
127
+
128
+ if message and not duplicate? exp
129
+ add_result exp
130
+
131
+ warn :template => @current_template,
132
+ :warning_type => "Cross Site Scripting",
133
+ :message => message,
134
+ :line => exp.line,
135
+ :code => exp,
136
+ :confidence => CONFIDENCE[:low]
137
+ end
138
+
139
+ @mark = @matched = false
140
+ end
141
+
142
+ exp
143
+ end
144
+
145
+ def actually_process_call exp
146
+ return if @matched
147
+ target = exp[1]
148
+ if sexp? target
149
+ target = process target
150
+ end
151
+
152
+ method = exp[2]
153
+ args = exp[3]
154
+
155
+ #Ignore safe items
156
+ if (target.nil? and (IGNORE_METHODS.include? method or method.to_s =~ IGNORE_LIKE)) or
157
+ (@matched == :model and IGNORE_MODEL_METHODS.include? method) or
158
+ (target == HAML_HELPERS and method == :html_escape) or
159
+ ((target == URI or target == CGI) and method == :escape) or
160
+ (target == FORM_BUILDER and IGNORE_METHODS.include? method) or
161
+ (method.to_s[-1,1] == "?")
162
+
163
+ exp[0] = :ignore
164
+ @matched = false
165
+ elsif sexp? exp[1] and model_name? exp[1][1]
166
+
167
+ @matched = :model
168
+ elsif @inspect_arguments and (ALL_PARAMETERS.include?(exp) or params? exp)
169
+
170
+ @matched = :params
171
+ else
172
+ process args if @inspect_arguments
173
+ end
174
+ end
175
+
176
+ #Note that params have been found
177
+ def process_params exp
178
+ @matched = :params
179
+ exp
180
+ end
181
+
182
+ #Note that cookies have been found
183
+ def process_cookies exp
184
+ @matched = :cookies
185
+ exp
186
+ end
187
+
188
+ #Ignore calls to render
189
+ def process_render exp
190
+ exp
191
+ end
192
+
193
+ #Process as default
194
+ def process_string_interp exp
195
+ process_default exp
196
+ end
197
+
198
+ #Process as default
199
+ def process_format exp
200
+ process_default exp
201
+ end
202
+
203
+ #Ignore output HTML escaped via HAML
204
+ def process_format_escaped exp
205
+ exp
206
+ end
207
+
208
+ #Ignore condition in if Sexp
209
+ def process_if exp
210
+ exp[2..-1].each do |e|
211
+ process e if sexp? e
212
+ end
213
+ exp
214
+ end
215
+
216
+ end