brakeman-lib 4.5.1 → 4.7.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +158 -109
  3. data/README.md +1 -2
  4. data/lib/brakeman/call_index.rb +54 -15
  5. data/lib/brakeman/checks/base_check.rb +50 -47
  6. data/lib/brakeman/checks/check_cookie_serialization.rb +22 -0
  7. data/lib/brakeman/checks/check_cross_site_scripting.rb +4 -4
  8. data/lib/brakeman/checks/check_deserialize.rb +3 -6
  9. data/lib/brakeman/checks/check_execute.rb +26 -1
  10. data/lib/brakeman/checks/check_file_access.rb +7 -1
  11. data/lib/brakeman/checks/check_header_dos.rb +2 -2
  12. data/lib/brakeman/checks/check_i18n_xss.rb +2 -2
  13. data/lib/brakeman/checks/check_jruby_xml.rb +2 -2
  14. data/lib/brakeman/checks/check_json_parsing.rb +2 -2
  15. data/lib/brakeman/checks/check_mass_assignment.rb +1 -1
  16. data/lib/brakeman/checks/check_mime_type_dos.rb +2 -2
  17. data/lib/brakeman/checks/check_nested_attributes_bypass.rb +1 -1
  18. data/lib/brakeman/checks/check_reverse_tabnabbing.rb +58 -0
  19. data/lib/brakeman/checks/check_sanitize_methods.rb +2 -2
  20. data/lib/brakeman/checks/check_session_settings.rb +5 -2
  21. data/lib/brakeman/checks/check_sql.rb +24 -22
  22. data/lib/brakeman/checks/check_xml_dos.rb +2 -2
  23. data/lib/brakeman/checks/check_yaml_parsing.rb +10 -18
  24. data/lib/brakeman/differ.rb +16 -28
  25. data/lib/brakeman/file_parser.rb +4 -8
  26. data/lib/brakeman/file_path.rb +14 -0
  27. data/lib/brakeman/parsers/haml_embedded.rb +1 -1
  28. data/lib/brakeman/parsers/template_parser.rb +3 -1
  29. data/lib/brakeman/processor.rb +2 -2
  30. data/lib/brakeman/processors/alias_processor.rb +15 -1
  31. data/lib/brakeman/processors/base_processor.rb +2 -0
  32. data/lib/brakeman/processors/controller_processor.rb +4 -4
  33. data/lib/brakeman/processors/gem_processor.rb +10 -2
  34. data/lib/brakeman/processors/haml_template_processor.rb +87 -123
  35. data/lib/brakeman/processors/lib/call_conversion_helper.rb +5 -4
  36. data/lib/brakeman/processors/lib/find_all_calls.rb +27 -4
  37. data/lib/brakeman/processors/lib/find_call.rb +3 -64
  38. data/lib/brakeman/processors/lib/rails2_config_processor.rb +1 -1
  39. data/lib/brakeman/processors/template_alias_processor.rb +28 -0
  40. data/lib/brakeman/processors/template_processor.rb +10 -6
  41. data/lib/brakeman/report/report_text.rb +4 -5
  42. data/lib/brakeman/rescanner.rb +4 -0
  43. data/lib/brakeman/tracker.rb +26 -2
  44. data/lib/brakeman/tracker/config.rb +38 -73
  45. data/lib/brakeman/tracker/constants.rb +2 -1
  46. data/lib/brakeman/util.rb +5 -3
  47. data/lib/brakeman/version.rb +1 -1
  48. data/lib/brakeman/warning.rb +4 -0
  49. data/lib/brakeman/warning_codes.rb +3 -0
  50. data/lib/ruby_parser/bm_sexp.rb +7 -2
  51. metadata +18 -17
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  [![Brakeman Logo](http://brakemanscanner.org/images/logo_medium.png)](http://brakemanscanner.org/)
2
2
 
3
3
  [![Build Status](https://circleci.com/gh/presidentbeef/brakeman.svg?style=svg)](https://circleci.com/gh/presidentbeef/brakeman)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/1b08a5c74695cb0d11ec/maintainability)](https://codeclimate.com/github/presidentbeef/brakeman/maintainability)
5
4
  [![Test Coverage](https://api.codeclimate.com/v1/badges/1b08a5c74695cb0d11ec/test_coverage)](https://codeclimate.com/github/presidentbeef/brakeman/test_coverage)
6
5
  [![Gitter](https://badges.gitter.im/presidentbeef/brakeman.svg)](https://gitter.im/presidentbeef/brakeman)
7
6
 
@@ -63,7 +62,7 @@ Outside of Rails root (note that the output file is relative to path/to/rails/ap
63
62
 
64
63
  # Compatibility
65
64
 
66
- Brakeman should work with any version of Rails from 2.3.x to 5.x.
65
+ Brakeman should work with any version of Rails from 2.3.x to 6.x.
67
66
 
68
67
  Brakeman can analyze code written with Ruby 1.8 syntax and newer, but requires at least Ruby 2.3.0 to run.
69
68
 
@@ -27,7 +27,7 @@ class Brakeman::CallIndex
27
27
  if options[:chained]
28
28
  return find_chain options
29
29
  #Find by narrowest category
30
- elsif target and method and target.is_a? Array and method.is_a? Array
30
+ elsif target.is_a? Array and method.is_a? Array
31
31
  if target.length > method.length
32
32
  calls = filter_by_target calls_by_methods(method), target
33
33
  else
@@ -35,6 +35,12 @@ class Brakeman::CallIndex
35
35
  calls = filter_by_method calls, method
36
36
  end
37
37
 
38
+ elsif target.is_a? Regexp and method
39
+ calls = filter_by_target(calls_by_method(method), target)
40
+
41
+ elsif method.is_a? Regexp and target
42
+ calls = filter_by_method(calls_by_target(target), method)
43
+
38
44
  #Find by target, then by methods, if provided
39
45
  elsif target
40
46
  calls = calls_by_target target
@@ -85,6 +91,16 @@ class Brakeman::CallIndex
85
91
  end
86
92
  end
87
93
 
94
+ def remove_indexes_by_file file
95
+ [@calls_by_method, @calls_by_target].each do |calls_by|
96
+ calls_by.each do |_name, calls|
97
+ calls.delete_if do |call|
98
+ call[:location][:file] == file
99
+ end
100
+ end
101
+ end
102
+ end
103
+
88
104
  def index_calls calls
89
105
  calls.each do |call|
90
106
  @calls_by_method[call[:method]] ||= []
@@ -116,8 +132,11 @@ class Brakeman::CallIndex
116
132
  end
117
133
 
118
134
  def calls_by_target target
119
- if target.is_a? Array
135
+ case target
136
+ when Array
120
137
  calls_by_targets target
138
+ when Regexp
139
+ calls_by_targets_regex target
121
140
  else
122
141
  @calls_by_target[target] || []
123
142
  end
@@ -133,10 +152,24 @@ class Brakeman::CallIndex
133
152
  calls
134
153
  end
135
154
 
155
+ def calls_by_targets_regex targets_regex
156
+ calls = []
157
+
158
+ @calls_by_target.each do |key, value|
159
+ case key
160
+ when String, Symbol
161
+ calls.concat value if key.match targets_regex
162
+ end
163
+ end
164
+
165
+ calls
166
+ end
167
+
136
168
  def calls_by_method method
137
- if method.is_a? Array
169
+ case method
170
+ when Array
138
171
  calls_by_methods method
139
- elsif method.is_a? Regexp
172
+ when Regexp
140
173
  calls_by_methods_regex method
141
174
  else
142
175
  @calls_by_method[method.to_sym] || []
@@ -156,26 +189,28 @@ class Brakeman::CallIndex
156
189
 
157
190
  def calls_by_methods_regex methods_regex
158
191
  calls = []
192
+
159
193
  @calls_by_method.each do |key, value|
160
- calls.concat value if key.to_s.match methods_regex
194
+ calls.concat value if key.match methods_regex
161
195
  end
162
- calls
163
- end
164
196
 
165
- def calls_with_no_target
166
- @calls_by_target[nil]
197
+ calls
167
198
  end
168
199
 
169
200
  def filter calls, key, value
170
- if value.is_a? Array
201
+ case value
202
+ when Array
171
203
  values = Set.new value
172
204
 
173
205
  calls.select do |call|
174
206
  values.include? call[key]
175
207
  end
176
- elsif value.is_a? Regexp
208
+ when Regexp
177
209
  calls.select do |call|
178
- call[key].to_s.match value
210
+ case call[key]
211
+ when String, Symbol
212
+ call[key].match value
213
+ end
179
214
  end
180
215
  else
181
216
  calls.select do |call|
@@ -197,15 +232,19 @@ class Brakeman::CallIndex
197
232
  end
198
233
 
199
234
  def filter_by_chain calls, target
200
- if target.is_a? Array
235
+ case target
236
+ when Array
201
237
  targets = Set.new target
202
238
 
203
239
  calls.select do |call|
204
240
  targets.include? call[:chain].first
205
241
  end
206
- elsif target.is_a? Regexp
242
+ when Regexp
207
243
  calls.select do |call|
208
- call[:chain].first.to_s.match target
244
+ case call[:chain].first
245
+ when String, Symbol
246
+ call[:chain].first.match target
247
+ end
209
248
  end
210
249
  else
211
250
  calls.select do |call|
@@ -39,24 +39,15 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
39
39
  @active_record_models = nil
40
40
  @mass_assign_disabled = nil
41
41
  @has_user_input = nil
42
+ @in_array = false
42
43
  @safe_input_attributes = Set[:to_i, :to_f, :arel_table, :id]
43
44
  @comparison_ops = Set[:==, :!=, :>, :<, :>=, :<=]
44
45
  end
45
46
 
46
47
  #Add result to result list, which is used to check for duplicates
47
- def add_result result, location = nil
48
- location ||= (@current_template && @current_template.name) || @current_class || @current_module || @current_set || result[:location][:class] || result[:location][:template]
49
- location = location[:name] if location.is_a? Hash
50
- location = location.name if location.is_a? Brakeman::Collection
51
- location = location.to_sym
52
-
53
- if result.is_a? Hash
54
- line = result[:call].original_line || result[:call].line
55
- elsif sexp? result
56
- line = result.original_line || result.line
57
- else
58
- raise ArgumentError
59
- end
48
+ def add_result result
49
+ location = get_location result
50
+ location, line = get_location result
60
51
 
61
52
  @results << [line, location, result]
62
53
  end
@@ -119,9 +110,16 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
119
110
  exp
120
111
  end
121
112
 
113
+ def process_array exp
114
+ @in_array = true
115
+ process_default exp
116
+ ensure
117
+ @in_array = false
118
+ end
119
+
122
120
  #Does not actually process string interpolation, but notes that it occurred.
123
121
  def process_dstr exp
124
- unless @string_interp # don't overwrite existing value
122
+ unless array_interp? exp or @string_interp # don't overwrite existing value
125
123
  @string_interp = Match.new(:interp, exp)
126
124
  end
127
125
 
@@ -130,6 +128,20 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
130
128
 
131
129
  private
132
130
 
131
+ # Checking for
132
+ #
133
+ # %W[#{a}]
134
+ #
135
+ # which will be parsed as
136
+ #
137
+ # s(:array, s(:dstr, "", s(:evstr, s(:call, nil, :a))))
138
+ def array_interp? exp
139
+ @in_array and
140
+ string_interp? exp and
141
+ exp[1] == "".freeze and
142
+ exp.length == 3 # only one interpolated value
143
+ end
144
+
133
145
  def always_safe_method? meth
134
146
  @safe_input_attributes.include? meth or
135
147
  @comparison_ops.include? meth
@@ -170,8 +182,9 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
170
182
  @mass_assign_disabled = true
171
183
  else
172
184
  #Check for ActiveRecord::Base.send(:attr_accessible, nil)
173
- tracker.check_initializers(:"ActiveRecord::Base", :attr_accessible).each do |result|
174
- call = result.call
185
+ tracker.find_call(target: :"ActiveRecord::Base", method: :attr_accessible).each do |result|
186
+ call = result[:call]
187
+
175
188
  if call? call
176
189
  if call.first_arg == Sexp.new(:nil)
177
190
  @mass_assign_disabled = true
@@ -180,26 +193,12 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
180
193
  end
181
194
  end
182
195
 
183
- unless @mass_assign_disabled
184
- tracker.check_initializers(:"ActiveRecord::Base", :send).each do |result|
185
- call = result.call
186
- if call? call
187
- if call.first_arg == Sexp.new(:lit, :attr_accessible) and call.second_arg == Sexp.new(:nil)
188
- @mass_assign_disabled = true
189
- break
190
- end
191
- end
192
- end
193
- end
194
-
195
196
  unless @mass_assign_disabled
196
197
  #Check for
197
198
  # class ActiveRecord::Base
198
199
  # attr_accessible nil
199
200
  # end
200
- matches = tracker.check_initializers([], :attr_accessible)
201
-
202
- matches.each do |result|
201
+ tracker.check_initializers([], :attr_accessible).each do |result|
203
202
  if result.module == "ActiveRecord" and result.result_class == :Base
204
203
  arg = result.call.first_arg
205
204
 
@@ -227,10 +226,8 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
227
226
  end
228
227
 
229
228
  unless @mass_assign_disabled
230
- matches = tracker.check_initializers(:"ActiveRecord::Base", [:send, :include])
231
-
232
- matches.each do |result|
233
- call = result.call
229
+ tracker.find_call(target: :"ActiveRecord::Base", method: [:send, :include]).each do |result|
230
+ call = result[:call]
234
231
  if call? call and (call.first_arg == forbidden_protection or call.second_arg == forbidden_protection)
235
232
  @mass_assign_disabled = true
236
233
  end
@@ -250,6 +247,22 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
250
247
  #This is to avoid reporting duplicates. Checks if the result has been
251
248
  #reported already from the same line number.
252
249
  def duplicate? result, location = nil
250
+ location, line = get_location result
251
+
252
+ @results.each do |r|
253
+ if r[0] == line and r[1] == location
254
+ if tracker.options[:combine_locations]
255
+ return true
256
+ elsif r[2] == result
257
+ return true
258
+ end
259
+ end
260
+ end
261
+
262
+ false
263
+ end
264
+
265
+ def get_location result
253
266
  if result.is_a? Hash
254
267
  line = result[:call].original_line || result[:call].line
255
268
  elsif sexp? result
@@ -258,23 +271,13 @@ class Brakeman::BaseCheck < Brakeman::SexpProcessor
258
271
  raise ArgumentError
259
272
  end
260
273
 
261
- location ||= (@current_template && @current_template.name) || @current_class || @current_module || @current_set || result[:location][:class] || result[:location][:template]
274
+ location ||= (@current_template && @current_template.name) || @current_class || @current_module || @current_set || result[:location][:class] || result[:location][:template] || result[:location][:file].to_s
262
275
 
263
276
  location = location[:name] if location.is_a? Hash
264
277
  location = location.name if location.is_a? Brakeman::Collection
265
278
  location = location.to_sym
266
279
 
267
- @results.each do |r|
268
- if r[0] == line and r[1] == location
269
- if tracker.options[:combine_locations]
270
- return true
271
- elsif r[2] == result
272
- return true
273
- end
274
- end
275
- end
276
-
277
- false
280
+ return location, line
278
281
  end
279
282
 
280
283
  #Checks if an expression contains string interpolation.
@@ -0,0 +1,22 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ class Brakeman::CheckCookieSerialization < Brakeman::BaseCheck
4
+ Brakeman::Checks.add self
5
+
6
+ @description = "Check for use of Marshal for cookie serialization"
7
+
8
+ def run_check
9
+ tracker.find_call(target: :'Rails.application.config.action_dispatch', method: :cookies_serializer=).each do |result|
10
+ setting = result[:call].first_arg
11
+
12
+ if symbol? setting and [:marshal, :hybrid].include? setting.value
13
+ warn :result => result,
14
+ :warning_type => "Remote Code Execution",
15
+ :warning_code => :unsafe_cookie_serialization,
16
+ :message => msg("Use of unsafe cookie serialization strategy ", msg_code(setting.value.inspect), " might lead to remote code execution"),
17
+ :confidence => :medium,
18
+ :link_path => "unsafe_deserialization"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -287,7 +287,7 @@ class Brakeman::CheckCrossSiteScripting < Brakeman::BaseCheck
287
287
 
288
288
  def setup
289
289
  @ignore_methods = Set[:==, :!=, :button_to, :check_box, :content_tag, :escapeHTML, :escape_once,
290
- :field_field, :fields_for, :h, :hidden_field,
290
+ :field_field, :fields_for, :form_for, :h, :hidden_field,
291
291
  :hidden_field, :hidden_field_tag, :image_tag, :label,
292
292
  :link_to, :mail_to, :radio_button, :select,
293
293
  :submit_tag, :text_area, :text_field,
@@ -316,11 +316,11 @@ class Brakeman::CheckCrossSiteScripting < Brakeman::BaseCheck
316
316
  end
317
317
 
318
318
  json_escape_on = false
319
- initializers = tracker.check_initializers :ActiveSupport, :escape_html_entities_in_json=
320
- initializers.each {|result| json_escape_on = true?(result.call.first_arg) }
319
+ initializers = tracker.find_call(target: :ActiveSupport, method: :escape_html_entities_in_json=)
320
+ initializers.each {|result| json_escape_on = true?(result[:call].first_arg) }
321
321
 
322
322
  if tracker.config.escape_html_entities_in_json?
323
- json_escape_on = true
323
+ json_escape_on = true
324
324
  elsif version_between? "4.0.0", "9.9.9"
325
325
  json_escape_on = true
326
326
  end
@@ -80,13 +80,10 @@ class Brakeman::CheckDeserialize < Brakeman::BaseCheck
80
80
  def oj_safe_default?
81
81
  safe_default = false
82
82
 
83
- # TODO: Can we just index initializers already??
84
- if tracker.check_initializers(:Oj, :mimic_JSON).any?
83
+ if tracker.find_call(target: :Oj, method: :mimic_JSON).any?
85
84
  safe_default = true
86
- end
87
-
88
- if result = tracker.check_initializers(:Oj, :default_options=).first
89
- options = result.call.first_arg
85
+ elsif result = tracker.find_call(target: :Oj, method: :default_options=).first
86
+ options = result[:call].first_arg
90
87
 
91
88
  if oj_safe_mode? options
92
89
  safe_default = true
@@ -21,6 +21,10 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
21
21
  SHELL_ESCAPE_MODULE_METHODS = Set[:escape, :join, :shellescape, :shelljoin]
22
22
  SHELL_ESCAPE_MIXIN_METHODS = Set[:shellescape, :shelljoin]
23
23
 
24
+ # These are common shells that are known to allow the execution of commands
25
+ # via a -c flag. See dash_c_shell_command? for more info.
26
+ KNOWN_SHELL_COMMANDS = Set["sh", "bash", "ksh", "csh", "tcsh", "zsh"]
27
+
24
28
  SHELLWORDS = s(:const, :Shellwords)
25
29
 
26
30
  #Check models, controllers, and views for command injection.
@@ -42,6 +46,8 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
42
46
  end
43
47
  end
44
48
 
49
+ private
50
+
45
51
  #Processes results from Tracker#find_call.
46
52
  def process_result result
47
53
  call = result[:call]
@@ -54,7 +60,17 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
54
60
  failure = include_user_input?(args) || dangerous_interp?(args)
55
61
  end
56
62
  when :system, :exec
57
- failure = include_user_input?(first_arg) || dangerous_interp?(first_arg)
63
+ # Normally, if we're in a `system` or `exec` call, we only are worried
64
+ # about shell injection when there's a single argument, because comma-
65
+ # separated arguments are always escaped by Ruby. However, an exception is
66
+ # when the first two arguments are something like "bash -c" because then
67
+ # the third argument is effectively the command being run and might be
68
+ # a malicious executable if it comes (partially or fully) from user input.
69
+ if dash_c_shell_command?(first_arg, call.second_arg)
70
+ failure = include_user_input?(args[3]) || dangerous_interp?(args[3])
71
+ else
72
+ failure = include_user_input?(first_arg) || dangerous_interp?(first_arg)
73
+ end
58
74
  else
59
75
  failure = include_user_input?(args) || dangerous_interp?(args)
60
76
  end
@@ -77,6 +93,15 @@ class Brakeman::CheckExecute < Brakeman::BaseCheck
77
93
  end
78
94
  end
79
95
 
96
+ # @return [Boolean] true iff the command given by `first_arg`, `second_arg`
97
+ # invokes a new shell process via `<shell_command> -c` (like `bash -c`)
98
+ def dash_c_shell_command?(first_arg, second_arg)
99
+ string?(first_arg) &&
100
+ KNOWN_SHELL_COMMANDS.include?(first_arg.value) &&
101
+ string?(second_arg) &&
102
+ second_arg.value == "-c"
103
+ end
104
+
80
105
  def check_open_calls
81
106
  tracker.find_call(:targets => [nil, :Kernel], :method => :open).each do |result|
82
107
  if match = dangerous_open_arg?(result[:call].first_arg)
@@ -32,7 +32,7 @@ class Brakeman::CheckFileAccess < Brakeman::BaseCheck
32
32
 
33
33
  file_name = call.first_arg
34
34
 
35
- return if called_on_tempfile?(file_name)
35
+ return if called_on_tempfile?(file_name) || sanitized?(file_name)
36
36
 
37
37
  if match = has_immediate_user_input?(file_name)
38
38
  confidence = :high
@@ -71,6 +71,12 @@ class Brakeman::CheckFileAccess < Brakeman::BaseCheck
71
71
  call?(file_name) && file_name.target == s(:const, :Tempfile)
72
72
  end
73
73
 
74
+ def sanitized? file
75
+ call?(file) &&
76
+ call?(file.target) &&
77
+ class_name(file.target.target) == :"ActiveStorage::Filename"
78
+ end
79
+
74
80
  def temp_file_method? exp
75
81
  if call? exp
76
82
  return true if exp.call_chain.include? :tempfile