railroader 4.3.4

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 (165) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES.md +1091 -0
  3. data/FEATURES +16 -0
  4. data/README.md +174 -0
  5. data/bin/railroader +8 -0
  6. data/lib/railroader/app_tree.rb +191 -0
  7. data/lib/railroader/call_index.rb +219 -0
  8. data/lib/railroader/checks/base_check.rb +505 -0
  9. data/lib/railroader/checks/check_basic_auth.rb +88 -0
  10. data/lib/railroader/checks/check_basic_auth_timing_attack.rb +33 -0
  11. data/lib/railroader/checks/check_content_tag.rb +200 -0
  12. data/lib/railroader/checks/check_create_with.rb +74 -0
  13. data/lib/railroader/checks/check_cross_site_scripting.rb +381 -0
  14. data/lib/railroader/checks/check_default_routes.rb +86 -0
  15. data/lib/railroader/checks/check_deserialize.rb +56 -0
  16. data/lib/railroader/checks/check_detailed_exceptions.rb +55 -0
  17. data/lib/railroader/checks/check_digest_dos.rb +38 -0
  18. data/lib/railroader/checks/check_divide_by_zero.rb +42 -0
  19. data/lib/railroader/checks/check_dynamic_finders.rb +48 -0
  20. data/lib/railroader/checks/check_escape_function.rb +21 -0
  21. data/lib/railroader/checks/check_evaluation.rb +35 -0
  22. data/lib/railroader/checks/check_execute.rb +189 -0
  23. data/lib/railroader/checks/check_file_access.rb +71 -0
  24. data/lib/railroader/checks/check_file_disclosure.rb +35 -0
  25. data/lib/railroader/checks/check_filter_skipping.rb +31 -0
  26. data/lib/railroader/checks/check_forgery_setting.rb +81 -0
  27. data/lib/railroader/checks/check_header_dos.rb +31 -0
  28. data/lib/railroader/checks/check_i18n_xss.rb +48 -0
  29. data/lib/railroader/checks/check_jruby_xml.rb +36 -0
  30. data/lib/railroader/checks/check_json_encoding.rb +47 -0
  31. data/lib/railroader/checks/check_json_parsing.rb +107 -0
  32. data/lib/railroader/checks/check_link_to.rb +132 -0
  33. data/lib/railroader/checks/check_link_to_href.rb +146 -0
  34. data/lib/railroader/checks/check_mail_to.rb +49 -0
  35. data/lib/railroader/checks/check_mass_assignment.rb +196 -0
  36. data/lib/railroader/checks/check_mime_type_dos.rb +39 -0
  37. data/lib/railroader/checks/check_model_attr_accessible.rb +55 -0
  38. data/lib/railroader/checks/check_model_attributes.rb +119 -0
  39. data/lib/railroader/checks/check_model_serialize.rb +67 -0
  40. data/lib/railroader/checks/check_nested_attributes.rb +38 -0
  41. data/lib/railroader/checks/check_nested_attributes_bypass.rb +58 -0
  42. data/lib/railroader/checks/check_number_to_currency.rb +74 -0
  43. data/lib/railroader/checks/check_permit_attributes.rb +43 -0
  44. data/lib/railroader/checks/check_quote_table_name.rb +40 -0
  45. data/lib/railroader/checks/check_redirect.rb +256 -0
  46. data/lib/railroader/checks/check_regex_dos.rb +68 -0
  47. data/lib/railroader/checks/check_render.rb +97 -0
  48. data/lib/railroader/checks/check_render_dos.rb +37 -0
  49. data/lib/railroader/checks/check_render_inline.rb +53 -0
  50. data/lib/railroader/checks/check_response_splitting.rb +21 -0
  51. data/lib/railroader/checks/check_route_dos.rb +42 -0
  52. data/lib/railroader/checks/check_safe_buffer_manipulation.rb +31 -0
  53. data/lib/railroader/checks/check_sanitize_methods.rb +112 -0
  54. data/lib/railroader/checks/check_secrets.rb +40 -0
  55. data/lib/railroader/checks/check_select_tag.rb +59 -0
  56. data/lib/railroader/checks/check_select_vulnerability.rb +60 -0
  57. data/lib/railroader/checks/check_send.rb +47 -0
  58. data/lib/railroader/checks/check_send_file.rb +19 -0
  59. data/lib/railroader/checks/check_session_manipulation.rb +35 -0
  60. data/lib/railroader/checks/check_session_settings.rb +176 -0
  61. data/lib/railroader/checks/check_simple_format.rb +58 -0
  62. data/lib/railroader/checks/check_single_quotes.rb +101 -0
  63. data/lib/railroader/checks/check_skip_before_filter.rb +60 -0
  64. data/lib/railroader/checks/check_sql.rb +700 -0
  65. data/lib/railroader/checks/check_sql_cves.rb +106 -0
  66. data/lib/railroader/checks/check_ssl_verify.rb +48 -0
  67. data/lib/railroader/checks/check_strip_tags.rb +89 -0
  68. data/lib/railroader/checks/check_symbol_dos.rb +71 -0
  69. data/lib/railroader/checks/check_symbol_dos_cve.rb +30 -0
  70. data/lib/railroader/checks/check_translate_bug.rb +45 -0
  71. data/lib/railroader/checks/check_unsafe_reflection.rb +50 -0
  72. data/lib/railroader/checks/check_unscoped_find.rb +57 -0
  73. data/lib/railroader/checks/check_validation_regex.rb +116 -0
  74. data/lib/railroader/checks/check_weak_hash.rb +148 -0
  75. data/lib/railroader/checks/check_without_protection.rb +80 -0
  76. data/lib/railroader/checks/check_xml_dos.rb +45 -0
  77. data/lib/railroader/checks/check_yaml_parsing.rb +121 -0
  78. data/lib/railroader/checks.rb +209 -0
  79. data/lib/railroader/codeclimate/engine_configuration.rb +97 -0
  80. data/lib/railroader/commandline.rb +179 -0
  81. data/lib/railroader/differ.rb +66 -0
  82. data/lib/railroader/file_parser.rb +54 -0
  83. data/lib/railroader/format/style.css +133 -0
  84. data/lib/railroader/options.rb +339 -0
  85. data/lib/railroader/parsers/rails2_erubis.rb +6 -0
  86. data/lib/railroader/parsers/rails2_xss_plugin_erubis.rb +48 -0
  87. data/lib/railroader/parsers/rails3_erubis.rb +81 -0
  88. data/lib/railroader/parsers/template_parser.rb +108 -0
  89. data/lib/railroader/processor.rb +102 -0
  90. data/lib/railroader/processors/alias_processor.rb +1229 -0
  91. data/lib/railroader/processors/base_processor.rb +295 -0
  92. data/lib/railroader/processors/config_processor.rb +14 -0
  93. data/lib/railroader/processors/controller_alias_processor.rb +278 -0
  94. data/lib/railroader/processors/controller_processor.rb +249 -0
  95. data/lib/railroader/processors/erb_template_processor.rb +77 -0
  96. data/lib/railroader/processors/erubis_template_processor.rb +92 -0
  97. data/lib/railroader/processors/gem_processor.rb +64 -0
  98. data/lib/railroader/processors/haml_template_processor.rb +191 -0
  99. data/lib/railroader/processors/lib/basic_processor.rb +37 -0
  100. data/lib/railroader/processors/lib/call_conversion_helper.rb +90 -0
  101. data/lib/railroader/processors/lib/find_all_calls.rb +224 -0
  102. data/lib/railroader/processors/lib/find_call.rb +183 -0
  103. data/lib/railroader/processors/lib/find_return_value.rb +166 -0
  104. data/lib/railroader/processors/lib/module_helper.rb +111 -0
  105. data/lib/railroader/processors/lib/processor_helper.rb +88 -0
  106. data/lib/railroader/processors/lib/rails2_config_processor.rb +145 -0
  107. data/lib/railroader/processors/lib/rails2_route_processor.rb +313 -0
  108. data/lib/railroader/processors/lib/rails3_config_processor.rb +132 -0
  109. data/lib/railroader/processors/lib/rails3_route_processor.rb +308 -0
  110. data/lib/railroader/processors/lib/render_helper.rb +181 -0
  111. data/lib/railroader/processors/lib/render_path.rb +107 -0
  112. data/lib/railroader/processors/lib/route_helper.rb +68 -0
  113. data/lib/railroader/processors/lib/safe_call_helper.rb +16 -0
  114. data/lib/railroader/processors/library_processor.rb +74 -0
  115. data/lib/railroader/processors/model_processor.rb +91 -0
  116. data/lib/railroader/processors/output_processor.rb +144 -0
  117. data/lib/railroader/processors/route_processor.rb +17 -0
  118. data/lib/railroader/processors/slim_template_processor.rb +111 -0
  119. data/lib/railroader/processors/template_alias_processor.rb +118 -0
  120. data/lib/railroader/processors/template_processor.rb +85 -0
  121. data/lib/railroader/report/config/remediation.yml +71 -0
  122. data/lib/railroader/report/ignore/config.rb +153 -0
  123. data/lib/railroader/report/ignore/interactive.rb +362 -0
  124. data/lib/railroader/report/pager.rb +112 -0
  125. data/lib/railroader/report/renderer.rb +24 -0
  126. data/lib/railroader/report/report_base.rb +292 -0
  127. data/lib/railroader/report/report_codeclimate.rb +79 -0
  128. data/lib/railroader/report/report_csv.rb +55 -0
  129. data/lib/railroader/report/report_hash.rb +23 -0
  130. data/lib/railroader/report/report_html.rb +216 -0
  131. data/lib/railroader/report/report_json.rb +45 -0
  132. data/lib/railroader/report/report_markdown.rb +107 -0
  133. data/lib/railroader/report/report_table.rb +117 -0
  134. data/lib/railroader/report/report_tabs.rb +17 -0
  135. data/lib/railroader/report/report_text.rb +198 -0
  136. data/lib/railroader/report/templates/controller_overview.html.erb +22 -0
  137. data/lib/railroader/report/templates/controller_warnings.html.erb +21 -0
  138. data/lib/railroader/report/templates/error_overview.html.erb +29 -0
  139. data/lib/railroader/report/templates/header.html.erb +58 -0
  140. data/lib/railroader/report/templates/ignored_warnings.html.erb +25 -0
  141. data/lib/railroader/report/templates/model_warnings.html.erb +21 -0
  142. data/lib/railroader/report/templates/overview.html.erb +38 -0
  143. data/lib/railroader/report/templates/security_warnings.html.erb +23 -0
  144. data/lib/railroader/report/templates/template_overview.html.erb +21 -0
  145. data/lib/railroader/report/templates/view_warnings.html.erb +34 -0
  146. data/lib/railroader/report/templates/warning_overview.html.erb +17 -0
  147. data/lib/railroader/report.rb +88 -0
  148. data/lib/railroader/rescanner.rb +483 -0
  149. data/lib/railroader/scanner.rb +321 -0
  150. data/lib/railroader/tracker/collection.rb +93 -0
  151. data/lib/railroader/tracker/config.rb +154 -0
  152. data/lib/railroader/tracker/constants.rb +171 -0
  153. data/lib/railroader/tracker/controller.rb +161 -0
  154. data/lib/railroader/tracker/library.rb +17 -0
  155. data/lib/railroader/tracker/model.rb +90 -0
  156. data/lib/railroader/tracker/template.rb +33 -0
  157. data/lib/railroader/tracker.rb +362 -0
  158. data/lib/railroader/util.rb +503 -0
  159. data/lib/railroader/version.rb +3 -0
  160. data/lib/railroader/warning.rb +294 -0
  161. data/lib/railroader/warning_codes.rb +117 -0
  162. data/lib/railroader.rb +544 -0
  163. data/lib/ruby_parser/bm_sexp.rb +626 -0
  164. data/lib/ruby_parser/bm_sexp_processor.rb +116 -0
  165. metadata +386 -0
@@ -0,0 +1,700 @@
1
+ require 'railroader/checks/base_check'
2
+
3
+ #This check tests for find calls which do not use Rails' auto SQL escaping
4
+ #
5
+ #For example:
6
+ # Project.find(:all, :conditions => "name = '" + params[:name] + "'")
7
+ #
8
+ # Project.find(:all, :conditions => "name = '#{params[:name]}'")
9
+ #
10
+ # User.find_by_sql("SELECT * FROM projects WHERE name = '#{params[:name]}'")
11
+ class Railroader::CheckSQL < Railroader::BaseCheck
12
+ Railroader::Checks.add self
13
+
14
+ @description = "Check for SQL injection"
15
+
16
+ def run_check
17
+ narrow_targets = [:exists?, :select]
18
+
19
+ @sql_targets = [:average, :calculate, :count, :count_by_sql, :delete_all, :destroy_all,
20
+ :find_by_sql, :maximum, :minimum, :pluck, :sum, :update_all]
21
+ @sql_targets.concat [:from, :group, :having, :joins, :lock, :order, :reorder, :where] if tracker.options[:rails3]
22
+ @sql_targets << :find_by << :find_by! << :not if tracker.options[:rails4]
23
+
24
+ if version_between?("2.0.0", "3.9.9") or tracker.config.rails_version.nil?
25
+ @sql_targets << :first << :last << :all
26
+ end
27
+
28
+ if version_between?("2.0.0", "4.0.99") or tracker.config.rails_version.nil?
29
+ @sql_targets << :find
30
+ end
31
+
32
+ @connection_calls = [:delete, :execute, :insert, :select_all, :select_one,
33
+ :select_rows, :select_value, :select_values]
34
+
35
+ if tracker.options[:rails3]
36
+ @connection_calls.concat [:exec_delete, :exec_insert, :exec_query, :exec_update]
37
+ else
38
+ @connection_calls.concat [:add_limit!, :add_offset_limit!, :add_lock!]
39
+ end
40
+
41
+ @expected_targets = active_record_models.keys + [:connection, :"ActiveRecord::Base", :Arel]
42
+
43
+ Railroader.debug "Finding possible SQL calls on models"
44
+ calls = tracker.find_call(:methods => @sql_targets, :nested => true)
45
+
46
+ calls.concat tracker.find_call(:targets => active_record_models.keys, :methods => narrow_targets, :chained => true)
47
+
48
+ Railroader.debug "Finding possible SQL calls with no target"
49
+ calls.concat tracker.find_call(:target => nil, :methods => @sql_targets)
50
+
51
+ Railroader.debug "Finding possible SQL calls using constantized()"
52
+ calls.concat tracker.find_call(:methods => @sql_targets).select { |result| constantize_call? result }
53
+
54
+ calls.concat tracker.find_call(:targets => @expected_targets, :methods => @connection_calls, :chained => true).select { |result| connect_call? result }
55
+
56
+ calls.concat tracker.find_call(:target => :Arel, :method => :sql)
57
+
58
+ Railroader.debug "Finding calls to named_scope or scope"
59
+ calls.concat find_scope_calls
60
+
61
+ Railroader.debug "Processing possible SQL calls"
62
+ calls.each { |call| process_result call }
63
+ end
64
+
65
+ #Find calls to named_scope() or scope() in models
66
+ #RP 3 TODO
67
+ def find_scope_calls
68
+ scope_calls = []
69
+
70
+ if version_between?("2.1.0", "3.0.9")
71
+ ar_scope_calls(:named_scope) do |name, args|
72
+ call = make_call(nil, :named_scope, args).line(args.line)
73
+ scope_calls << scope_call_hash(call, name, :named_scope)
74
+ end
75
+ elsif version_between?("3.1.0", "9.9.9")
76
+ ar_scope_calls(:scope) do |name, args|
77
+ second_arg = args[2]
78
+ next unless sexp? second_arg
79
+
80
+ if second_arg.node_type == :iter and node_type? second_arg.block, :block, :call, :safe_call
81
+ process_scope_with_block(name, args)
82
+ elsif call? second_arg
83
+ call = second_arg
84
+ scope_calls << scope_call_hash(call, name, call.method)
85
+ else
86
+ call = make_call(nil, :scope, args).line(args.line)
87
+ scope_calls << scope_call_hash(call, name, :scope)
88
+ end
89
+ end
90
+ end
91
+
92
+ scope_calls
93
+ end
94
+
95
+ def ar_scope_calls(symbol_name = :named_scope, &block)
96
+ return_array = []
97
+ active_record_models.each do |name, model|
98
+ model_args = model.options[symbol_name]
99
+ if model_args
100
+ model_args.each do |args|
101
+ yield name, args
102
+ return_array << [name, args]
103
+ end
104
+ end
105
+ end
106
+ return_array
107
+ end
108
+
109
+ def scope_call_hash(call, name, method)
110
+ { :call => call, :location => { :type => :class, :class => name }, :method => :named_scope }
111
+ end
112
+
113
+
114
+ def process_scope_with_block model_name, args
115
+ scope_name = args[1][1]
116
+ block = args[-1][-1]
117
+
118
+ # Search lambda for calls to query methods
119
+ if block.node_type == :block
120
+ find_calls = Railroader::FindAllCalls.new(tracker)
121
+ find_calls.process_source(block, :class => model_name, :method => scope_name)
122
+ find_calls.calls.each { |call| process_result(call) if @sql_targets.include?(call[:method]) }
123
+ elsif call? block
124
+ while call? block
125
+ process_result :target => block.target, :method => block.method, :call => block,
126
+ :location => { :type => :class, :class => model_name, :method => scope_name }
127
+
128
+ block = block.target
129
+ end
130
+ end
131
+ end
132
+
133
+ #Process possible SQL injection sites:
134
+ #
135
+ # Model#find
136
+ #
137
+ # Model#(named_)scope
138
+ #
139
+ # Model#(find|count)_by_sql
140
+ #
141
+ # Model#all
142
+ #
143
+ ### Rails 3
144
+ #
145
+ # Model#(where|having)
146
+ # Model#(order|group)
147
+ #
148
+ ### Find Options Hash
149
+ #
150
+ # Dangerous keys that accept SQL:
151
+ #
152
+ # * conditions
153
+ # * order
154
+ # * having
155
+ # * joins
156
+ # * select
157
+ # * from
158
+ # * lock
159
+ #
160
+ def process_result result
161
+ return if duplicate?(result) or result[:call].original_line
162
+
163
+ call = result[:call]
164
+ method = call.method
165
+
166
+ dangerous_value = case method
167
+ when :find
168
+ check_find_arguments call.second_arg
169
+ when :exists?
170
+ check_exists call.first_arg
171
+ when :delete_all, :destroy_all
172
+ check_find_arguments call.first_arg
173
+ when :named_scope, :scope
174
+ check_scope_arguments call
175
+ when :find_by_sql, :count_by_sql
176
+ check_by_sql_arguments call.first_arg
177
+ when :calculate
178
+ check_find_arguments call.third_arg
179
+ when :last, :first, :all
180
+ check_find_arguments call.first_arg
181
+ when :average, :count, :maximum, :minimum, :sum
182
+ if call.length > 5
183
+ unsafe_sql?(call.first_arg) or check_find_arguments(call.last_arg)
184
+ else
185
+ check_find_arguments call.last_arg
186
+ end
187
+ when :where, :having, :find_by, :find_by!, :not
188
+ check_query_arguments call.arglist
189
+ when :order, :group, :reorder
190
+ check_order_arguments call.arglist
191
+ when :joins
192
+ check_joins_arguments call.first_arg
193
+ when :from
194
+ unsafe_sql? call.first_arg
195
+ when :lock
196
+ check_lock_arguments call.first_arg
197
+ when :pluck
198
+ unsafe_sql? call.first_arg
199
+ when :sql
200
+ unsafe_sql? call.first_arg
201
+ when :update_all, :select
202
+ check_update_all_arguments call.args
203
+ when *@connection_calls
204
+ check_by_sql_arguments call.first_arg
205
+ else
206
+ Railroader.debug "Unhandled SQL method: #{method}"
207
+ end
208
+
209
+ if dangerous_value
210
+ add_result result
211
+
212
+ input = include_user_input? dangerous_value
213
+ if input
214
+ confidence = :high
215
+ user_input = input
216
+ else
217
+ confidence = :medium
218
+ user_input = dangerous_value
219
+ end
220
+
221
+ if result[:call].target and result[:chain] and not @expected_targets.include? result[:chain].first
222
+ confidence = case confidence
223
+ when :high
224
+ :medium
225
+ when :medium
226
+ :weak
227
+ else
228
+ confidence
229
+ end
230
+ end
231
+
232
+ warn :result => result,
233
+ :warning_type => "SQL Injection",
234
+ :warning_code => :sql_injection,
235
+ :message => "Possible SQL injection",
236
+ :user_input => user_input,
237
+ :confidence => confidence
238
+ end
239
+
240
+ if check_for_limit_or_offset_vulnerability call.last_arg
241
+ if include_user_input? call.last_arg
242
+ confidence = :high
243
+ else
244
+ confidence = :weak
245
+ end
246
+
247
+ warn :result => result,
248
+ :warning_type => "SQL Injection",
249
+ :warning_code => :sql_injection_limit_offset,
250
+ :message => "Upgrade to Rails >= 2.1.2 to escape :limit and :offset. Possible SQL injection",
251
+ :confidence => confidence
252
+ end
253
+ end
254
+
255
+
256
+ #The 'find' methods accept a number of different types of parameters:
257
+ #
258
+ # * The first argument might be :all, :first, or :last
259
+ # * The first argument might be an integer ID or an array of IDs
260
+ # * The second argument might be a hash of options, some of which are
261
+ # dangerous and some of which are not
262
+ # * The second argument might contain SQL fragments as values
263
+ # * The second argument might contain properly parameterized SQL fragments in arrays
264
+ # * The second argument might contain improperly parameterized SQL fragments in arrays
265
+ #
266
+ #This method should only be passed the second argument.
267
+ def check_find_arguments arg
268
+ return nil if not sexp? arg or node_type? arg, :lit, :string, :str, :true, :false, :nil
269
+
270
+ unsafe_sql? arg
271
+ end
272
+
273
+ def check_scope_arguments call
274
+ scope_arg = call.second_arg #first arg is name of scope
275
+
276
+ node_type?(scope_arg, :iter) ? unsafe_sql?(scope_arg.block) : unsafe_sql?(scope_arg)
277
+ end
278
+
279
+ def check_query_arguments arg
280
+ return unless sexp? arg
281
+ first_arg = arg[1]
282
+
283
+ if node_type? arg, :arglist
284
+ if arg.length > 2 and string_interp? first_arg
285
+ # Model.where("blah = ?", blah)
286
+ return check_string_interp first_arg
287
+ else
288
+ arg = first_arg
289
+ end
290
+ end
291
+
292
+ if request_value? arg
293
+ unless call? arg and params? arg.target and [:permit, :slice, :to_h, :to_hash, :symbolize_keys].include? arg.method
294
+ # Model.where(params[:where])
295
+ arg
296
+ end
297
+ elsif hash? arg
298
+ #This is generally going to be a hash of column names and values, which
299
+ #would escape the values. But the keys _could_ be user input.
300
+ check_hash_keys arg
301
+ elsif node_type? arg, :lit, :str
302
+ nil
303
+ else
304
+ #Hashes are safe...but we check above for hash, so...?
305
+ unsafe_sql? arg, :ignore_hash
306
+ end
307
+ end
308
+
309
+ #Checks each argument to order/reorder/group for possible SQL.
310
+ #Anything used with these methods is passed in verbatim.
311
+ def check_order_arguments args
312
+ return unless sexp? args
313
+
314
+ if node_type? args, :arglist
315
+ check_update_all_arguments(args)
316
+ else
317
+ unsafe_sql? args
318
+ end
319
+ end
320
+
321
+ #find_by_sql and count_by_sql can take either a straight SQL string
322
+ #or an array with values to bind.
323
+ def check_by_sql_arguments arg
324
+ return unless sexp? arg
325
+
326
+ #This is kind of unnecessary, because unsafe_sql? will handle an array
327
+ #correctly, but might be better to be explicit.
328
+ array?(arg) ? unsafe_sql?(arg[1]) : unsafe_sql?(arg)
329
+ end
330
+
331
+ #joins can take a string, hash of associations, or an array of both(?)
332
+ #We only care about the possible string values.
333
+ def check_joins_arguments arg
334
+ return unless sexp? arg and not node_type? arg, :hash, :string, :str
335
+
336
+ if array? arg
337
+ arg.each do |a|
338
+ unsafe_arg = check_joins_arguments a
339
+ return unsafe_arg if unsafe_arg
340
+ end
341
+
342
+ nil
343
+ else
344
+ unsafe_sql? arg
345
+ end
346
+ end
347
+
348
+ def check_update_all_arguments args
349
+ args.each do |arg|
350
+ unsafe_arg = unsafe_sql? arg
351
+ return unsafe_arg if unsafe_arg
352
+ end
353
+
354
+ nil
355
+ end
356
+
357
+ #Model#lock essentially only cares about strings. But those strings can be
358
+ #any SQL fragment. This does not apply to all databases. (For those who do not
359
+ #support it, the lock method does nothing).
360
+ def check_lock_arguments arg
361
+ return unless sexp? arg and not node_type? arg, :hash, :array, :string, :str
362
+
363
+ unsafe_sql?(arg, :ignore_hash)
364
+ end
365
+
366
+
367
+ #Check hash keys for user input.
368
+ #(Seems unlikely, but if a user can control the column names queried, that
369
+ #could be bad)
370
+ def check_hash_keys exp
371
+ hash_iterate(exp) do |key, _value|
372
+ unless symbol?(key)
373
+ unsafe_key = unsafe_sql? key
374
+ return unsafe_key if unsafe_key
375
+ end
376
+ end
377
+
378
+ false
379
+ end
380
+
381
+ #Check an interpolated string for dangerous values.
382
+ #
383
+ #This method assumes values interpolated into strings are unsafe by default,
384
+ #unless safe_value? explicitly returns true.
385
+ def check_string_interp arg
386
+ arg.each do |exp|
387
+ if dangerous = unsafe_string_interp?(exp)
388
+ return dangerous
389
+ end
390
+ end
391
+
392
+ nil
393
+ end
394
+
395
+ #Returns value if interpolated value is not something safe
396
+ def unsafe_string_interp? exp
397
+ if node_type? exp, :evstr
398
+ value = exp.value
399
+ else
400
+ value = exp
401
+ end
402
+
403
+ if not sexp? value
404
+ nil
405
+ elsif call? value and value.method == :to_s
406
+ unsafe_string_interp? value.target
407
+ elsif call? value and safe_literal_target? value
408
+ nil
409
+ else
410
+ case value.node_type
411
+ when :or
412
+ unsafe_string_interp?(value.lhs) || unsafe_string_interp?(value.rhs)
413
+ when :dstr
414
+ if dangerous = check_string_interp(value)
415
+ return dangerous
416
+ end
417
+ else
418
+ if safe_value? value
419
+ nil
420
+ elsif string_building? value
421
+ check_for_string_building value
422
+ else
423
+ value
424
+ end
425
+ end
426
+ end
427
+ end
428
+
429
+ #Checks the given expression for unsafe SQL values. If an unsafe value is
430
+ #found, returns that value (may be the given _exp_ or a subexpression).
431
+ #
432
+ #Otherwise, returns false/nil.
433
+ def unsafe_sql? exp, ignore_hash = false
434
+ return unless sexp?(exp)
435
+
436
+ dangerous_value = find_dangerous_value exp, ignore_hash
437
+ safe_value?(dangerous_value) ? false : dangerous_value
438
+ end
439
+
440
+ #Check _exp_ for dangerous values. Used by unsafe_sql?
441
+ def find_dangerous_value exp, ignore_hash
442
+ case exp.node_type
443
+ when :lit, :str, :const, :colon2, :true, :false, :nil
444
+ nil
445
+ when :array
446
+ #Assume this is an array like
447
+ #
448
+ # ["blah = ? AND thing = ?", ...]
449
+ #
450
+ #and check first value
451
+ unsafe_sql? exp[1]
452
+ when :dstr
453
+ check_string_interp exp
454
+ when :hash
455
+ check_hash_values exp unless ignore_hash
456
+ when :if
457
+ unsafe_sql? exp.then_clause or unsafe_sql? exp.else_clause
458
+ when :call
459
+ unless IGNORE_METHODS_IN_SQL.include? exp.method
460
+ if has_immediate_user_input? exp
461
+ exp
462
+ elsif exp.method == :to_s
463
+ find_dangerous_value exp.target, ignore_hash
464
+ else
465
+ check_call exp
466
+ end
467
+ end
468
+ when :or
469
+ if unsafe = (unsafe_sql?(exp.lhs) || unsafe_sql?(exp.rhs))
470
+ unsafe
471
+ else
472
+ nil
473
+ end
474
+ when :block, :rlist
475
+ unsafe_sql? exp.last
476
+ else
477
+ if has_immediate_user_input? exp
478
+ exp
479
+ else
480
+ nil
481
+ end
482
+ end
483
+ end
484
+
485
+ #Checks hash values associated with these keys:
486
+ #
487
+ # * conditions
488
+ # * order
489
+ # * having
490
+ # * joins
491
+ # * select
492
+ # * from
493
+ # * lock
494
+ def check_hash_values exp
495
+ hash_iterate(exp) do |key, value|
496
+ if symbol? key
497
+ unsafe = case key.value
498
+ when :conditions, :having, :select
499
+ check_query_arguments value
500
+ when :order, :group
501
+ check_order_arguments value
502
+ when :joins
503
+ check_joins_arguments value
504
+ when :lock
505
+ check_lock_arguments value
506
+ when :from
507
+ unsafe_sql? value
508
+ else
509
+ nil
510
+ end
511
+
512
+ return unsafe if unsafe
513
+ end
514
+ end
515
+
516
+ false
517
+ end
518
+
519
+ STRING_METHODS = Set[:<<, :+, :concat, :prepend]
520
+
521
+ def check_for_string_building exp
522
+ return unless call? exp
523
+
524
+ target = exp.target
525
+ method = exp.method
526
+ arg = exp.first_arg
527
+
528
+ if STRING_METHODS.include? method
529
+ check_str_target_or_arg(target, arg) or
530
+ check_interp_target_or_arg(target, arg) or
531
+ check_for_string_building(target) or
532
+ check_for_string_building(arg)
533
+ else
534
+ nil
535
+ end
536
+ end
537
+
538
+ def check_str_target_or_arg target, arg
539
+ if string? target
540
+ check_string_arg arg
541
+ elsif string? arg
542
+ check_string_arg target
543
+ end
544
+ end
545
+
546
+ def check_interp_target_or_arg target, arg
547
+ if string_interp? target or string_interp? arg
548
+ check_string_arg target and
549
+ check_string_arg arg
550
+ end
551
+ end
552
+
553
+ def check_string_arg exp
554
+ if safe_value? exp
555
+ nil
556
+ elsif string_building? exp
557
+ check_for_string_building exp
558
+ elsif string_interp? exp
559
+ check_string_interp exp
560
+ elsif call? exp and exp.method == :to_s
561
+ check_string_arg exp.target
562
+ else
563
+ exp
564
+ end
565
+ end
566
+
567
+ def string_building? exp
568
+ return false unless call? exp and STRING_METHODS.include? exp.method
569
+
570
+ node_type? exp.target, :str, :dstr or
571
+ node_type? exp.first_arg, :str, :dstr or
572
+ string_building? exp.target or
573
+ string_building? exp.first_arg
574
+ end
575
+
576
+ IGNORE_METHODS_IN_SQL = Set[:id, :merge_conditions, :table_name, :quoted_table_name,
577
+ :quoted_primary_key, :to_i, :to_f, :sanitize_sql, :sanitize_sql_array,
578
+ :sanitize_sql_for_assignment, :sanitize_sql_for_conditions, :sanitize_sql_hash,
579
+ :sanitize_sql_hash_for_assignment, :sanitize_sql_hash_for_conditions,
580
+ :to_sql, :sanitize, :primary_key, :table_name_prefix, :table_name_suffix,
581
+ :where_values_hash, :foreign_key
582
+ ]
583
+
584
+ def safe_value? exp
585
+ return true unless sexp? exp
586
+
587
+ case exp.node_type
588
+ when :str, :lit, :const, :colon2, :nil, :true, :false
589
+ true
590
+ when :call
591
+ if exp.method == :to_s or exp.method == :to_sym
592
+ safe_value? exp.target
593
+ else
594
+ IGNORE_METHODS_IN_SQL.include? exp.method or
595
+ quote_call? exp or
596
+ arel? exp or
597
+ exp.method.to_s.end_with? "_id"
598
+ end
599
+ when :if
600
+ safe_value? exp.then_clause and safe_value? exp.else_clause
601
+ when :block, :rlist
602
+ safe_value? exp.last
603
+ when :or
604
+ safe_value? exp.lhs and safe_value? exp.rhs
605
+ when :dstr
606
+ not unsafe_string_interp? exp
607
+ else
608
+ false
609
+ end
610
+ end
611
+
612
+ QUOTE_METHODS = [:quote, :quote_column_name, :quoted_date, :quote_string, :quote_table_name]
613
+
614
+ def quote_call? exp
615
+ if call? exp.target
616
+ exp.target.method == :connection and QUOTE_METHODS.include? exp.method
617
+ elsif exp.target.nil?
618
+ exp.method == :quote_value
619
+ end
620
+ end
621
+
622
+ AREL_METHODS = [:all, :and, :arel_table, :as, :eq, :eq_any, :exists, :group,
623
+ :gt, :gteq, :having, :in, :join_sources, :limit, :lt, :lteq, :not,
624
+ :not_eq, :on, :or, :order, :project, :skip, :take, :where, :with]
625
+
626
+ def arel? exp
627
+ call? exp and (AREL_METHODS.include? exp.method or arel? exp.target)
628
+ end
629
+
630
+ #Check call for string building
631
+ def check_call exp
632
+ return unless call? exp
633
+ unsafe = check_for_string_building exp
634
+
635
+ if unsafe
636
+ unsafe
637
+ elsif call? exp.target
638
+ check_call exp.target
639
+ else
640
+ nil
641
+ end
642
+ end
643
+
644
+ def check_exists arg
645
+ if call? arg and arg.method == :to_s
646
+ false
647
+ else
648
+ check_find_arguments arg
649
+ end
650
+ end
651
+
652
+ #Prior to Rails 2.1.1, the :offset and :limit parameters were not
653
+ #escaping input properly.
654
+ #
655
+ #http://www.rorsecurity.info/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter/
656
+ def check_for_limit_or_offset_vulnerability options
657
+ return false if rails_version.nil? or rails_version >= "2.1.1" or not hash?(options)
658
+
659
+ return true if hash_access(options, :limit) or hash_access(options, :offset)
660
+
661
+ false
662
+ end
663
+
664
+ #Look for something like this:
665
+ #
666
+ # params[:x].constantize.find('something')
667
+ #
668
+ # s(:call,
669
+ # s(:call,
670
+ # s(:call,
671
+ # s(:call, nil, :params, s(:arglist)),
672
+ # :[],
673
+ # s(:arglist, s(:lit, :x))),
674
+ # :constantize,
675
+ # s(:arglist)),
676
+ # :find,
677
+ # s(:arglist, s(:str, "something")))
678
+ def constantize_call? result
679
+ call = result[:call]
680
+ call? call.target and call.target.method == :constantize
681
+ end
682
+
683
+ SELF_CLASS = s(:call, s(:self), :class)
684
+
685
+ def connect_call? result
686
+ call = result[:call]
687
+ target = call.target
688
+
689
+ if call? target and target.method == :connection
690
+ target = target.target
691
+ klass = class_name(target)
692
+
693
+ target.nil? or
694
+ target == SELF_CLASS or
695
+ node_type? target, :self or
696
+ klass == :"ActiveRecord::Base" or
697
+ active_record_models.include? klass
698
+ end
699
+ end
700
+ end