brakeman-min 0.5.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. data/CHANGES +529 -0
  2. data/README.md +74 -28
  3. data/bin/brakeman +60 -266
  4. data/lib/brakeman.rb +422 -0
  5. data/lib/brakeman/app_tree.rb +101 -0
  6. data/lib/brakeman/brakeman.rake +10 -0
  7. data/lib/brakeman/call_index.rb +215 -0
  8. data/lib/brakeman/checks.rb +180 -0
  9. data/lib/brakeman/checks/base_check.rb +538 -0
  10. data/lib/brakeman/checks/check_basic_auth.rb +89 -0
  11. data/lib/brakeman/checks/check_content_tag.rb +162 -0
  12. data/lib/brakeman/checks/check_cross_site_scripting.rb +334 -0
  13. data/lib/{checks → brakeman/checks}/check_default_routes.rb +13 -6
  14. data/lib/brakeman/checks/check_deserialize.rb +57 -0
  15. data/lib/brakeman/checks/check_digest_dos.rb +38 -0
  16. data/lib/brakeman/checks/check_escape_function.rb +21 -0
  17. data/lib/brakeman/checks/check_evaluation.rb +33 -0
  18. data/lib/brakeman/checks/check_execute.rb +98 -0
  19. data/lib/brakeman/checks/check_file_access.rb +62 -0
  20. data/lib/brakeman/checks/check_filter_skipping.rb +31 -0
  21. data/lib/brakeman/checks/check_forgery_setting.rb +54 -0
  22. data/lib/brakeman/checks/check_jruby_xml.rb +38 -0
  23. data/lib/brakeman/checks/check_json_parsing.rb +102 -0
  24. data/lib/brakeman/checks/check_link_to.rb +132 -0
  25. data/lib/brakeman/checks/check_link_to_href.rb +92 -0
  26. data/lib/{checks → brakeman/checks}/check_mail_to.rb +14 -13
  27. data/lib/brakeman/checks/check_mass_assignment.rb +143 -0
  28. data/lib/brakeman/checks/check_model_attr_accessible.rb +48 -0
  29. data/lib/brakeman/checks/check_model_attributes.rb +118 -0
  30. data/lib/brakeman/checks/check_model_serialize.rb +66 -0
  31. data/lib/{checks → brakeman/checks}/check_nested_attributes.rb +10 -6
  32. data/lib/brakeman/checks/check_quote_table_name.rb +40 -0
  33. data/lib/brakeman/checks/check_redirect.rb +177 -0
  34. data/lib/brakeman/checks/check_render.rb +62 -0
  35. data/lib/brakeman/checks/check_response_splitting.rb +21 -0
  36. data/lib/brakeman/checks/check_safe_buffer_manipulation.rb +31 -0
  37. data/lib/brakeman/checks/check_sanitize_methods.rb +54 -0
  38. data/lib/brakeman/checks/check_select_tag.rb +60 -0
  39. data/lib/brakeman/checks/check_select_vulnerability.rb +58 -0
  40. data/lib/brakeman/checks/check_send.rb +35 -0
  41. data/lib/brakeman/checks/check_send_file.rb +19 -0
  42. data/lib/brakeman/checks/check_session_settings.rb +145 -0
  43. data/lib/brakeman/checks/check_single_quotes.rb +101 -0
  44. data/lib/brakeman/checks/check_skip_before_filter.rb +62 -0
  45. data/lib/brakeman/checks/check_sql.rb +577 -0
  46. data/lib/brakeman/checks/check_strip_tags.rb +64 -0
  47. data/lib/brakeman/checks/check_symbol_dos.rb +67 -0
  48. data/lib/brakeman/checks/check_translate_bug.rb +45 -0
  49. data/lib/brakeman/checks/check_unsafe_reflection.rb +51 -0
  50. data/lib/brakeman/checks/check_validation_regex.rb +88 -0
  51. data/lib/brakeman/checks/check_without_protection.rb +64 -0
  52. data/lib/brakeman/checks/check_yaml_parsing.rb +121 -0
  53. data/lib/brakeman/differ.rb +66 -0
  54. data/lib/{format → brakeman/format}/style.css +28 -0
  55. data/lib/brakeman/options.rb +256 -0
  56. data/lib/brakeman/parsers/rails2_erubis.rb +6 -0
  57. data/lib/brakeman/parsers/rails2_xss_plugin_erubis.rb +48 -0
  58. data/lib/{scanner_erubis.rb → brakeman/parsers/rails3_erubis.rb} +8 -21
  59. data/lib/brakeman/processor.rb +102 -0
  60. data/lib/brakeman/processors/alias_processor.rb +780 -0
  61. data/lib/{processors → brakeman/processors}/base_processor.rb +90 -74
  62. data/lib/brakeman/processors/config_processor.rb +14 -0
  63. data/lib/brakeman/processors/controller_alias_processor.rb +334 -0
  64. data/lib/brakeman/processors/controller_processor.rb +265 -0
  65. data/lib/{processors → brakeman/processors}/erb_template_processor.rb +21 -19
  66. data/lib/brakeman/processors/erubis_template_processor.rb +96 -0
  67. data/lib/brakeman/processors/gem_processor.rb +59 -0
  68. data/lib/{processors → brakeman/processors}/haml_template_processor.rb +26 -21
  69. data/lib/brakeman/processors/lib/find_all_calls.rb +185 -0
  70. data/lib/{processors → brakeman/processors}/lib/find_call.rb +23 -28
  71. data/lib/brakeman/processors/lib/find_return_value.rb +134 -0
  72. data/lib/brakeman/processors/lib/processor_helper.rb +82 -0
  73. data/lib/{processors/config_processor.rb → brakeman/processors/lib/rails2_config_processor.rb} +32 -35
  74. data/lib/{processors → brakeman/processors}/lib/rails2_route_processor.rb +60 -52
  75. data/lib/brakeman/processors/lib/rails3_config_processor.rb +129 -0
  76. data/lib/brakeman/processors/lib/rails3_route_processor.rb +282 -0
  77. data/lib/{processors → brakeman/processors}/lib/render_helper.rb +54 -20
  78. data/lib/brakeman/processors/lib/route_helper.rb +62 -0
  79. data/lib/{processors → brakeman/processors}/library_processor.rb +24 -17
  80. data/lib/{processors → brakeman/processors}/model_processor.rb +46 -22
  81. data/lib/{processors → brakeman/processors}/output_processor.rb +34 -40
  82. data/lib/brakeman/processors/route_processor.rb +17 -0
  83. data/lib/brakeman/processors/slim_template_processor.rb +113 -0
  84. data/lib/brakeman/processors/template_alias_processor.rb +120 -0
  85. data/lib/{processors → brakeman/processors}/template_processor.rb +10 -7
  86. data/lib/brakeman/report.rb +68 -0
  87. data/lib/brakeman/report/ignore/config.rb +130 -0
  88. data/lib/brakeman/report/ignore/interactive.rb +311 -0
  89. data/lib/brakeman/report/initializers/faster_csv.rb +7 -0
  90. data/lib/brakeman/report/initializers/multi_json.rb +29 -0
  91. data/lib/brakeman/report/renderer.rb +24 -0
  92. data/lib/brakeman/report/report_base.rb +279 -0
  93. data/lib/brakeman/report/report_csv.rb +56 -0
  94. data/lib/brakeman/report/report_hash.rb +22 -0
  95. data/lib/brakeman/report/report_html.rb +203 -0
  96. data/lib/brakeman/report/report_json.rb +46 -0
  97. data/lib/brakeman/report/report_table.rb +109 -0
  98. data/lib/brakeman/report/report_tabs.rb +17 -0
  99. data/lib/brakeman/report/templates/controller_overview.html.erb +18 -0
  100. data/lib/brakeman/report/templates/controller_warnings.html.erb +17 -0
  101. data/lib/brakeman/report/templates/error_overview.html.erb +25 -0
  102. data/lib/brakeman/report/templates/header.html.erb +44 -0
  103. data/lib/brakeman/report/templates/ignored_warnings.html.erb +21 -0
  104. data/lib/brakeman/report/templates/model_warnings.html.erb +17 -0
  105. data/lib/brakeman/report/templates/overview.html.erb +34 -0
  106. data/lib/brakeman/report/templates/security_warnings.html.erb +19 -0
  107. data/lib/brakeman/report/templates/template_overview.html.erb +17 -0
  108. data/lib/brakeman/report/templates/view_warnings.html.erb +30 -0
  109. data/lib/brakeman/report/templates/warning_overview.html.erb +13 -0
  110. data/lib/brakeman/rescanner.rb +446 -0
  111. data/lib/brakeman/scanner.rb +362 -0
  112. data/lib/brakeman/tracker.rb +296 -0
  113. data/lib/brakeman/util.rb +413 -0
  114. data/lib/brakeman/version.rb +3 -0
  115. data/lib/brakeman/warning.rb +217 -0
  116. data/lib/brakeman/warning_codes.rb +68 -0
  117. data/lib/ruby_parser/bm_sexp.rb +562 -0
  118. data/lib/ruby_parser/bm_sexp_processor.rb +230 -0
  119. metadata +152 -66
  120. data/lib/checks.rb +0 -71
  121. data/lib/checks/base_check.rb +0 -357
  122. data/lib/checks/check_cross_site_scripting.rb +0 -336
  123. data/lib/checks/check_evaluation.rb +0 -27
  124. data/lib/checks/check_execute.rb +0 -110
  125. data/lib/checks/check_file_access.rb +0 -46
  126. data/lib/checks/check_forgery_setting.rb +0 -42
  127. data/lib/checks/check_mass_assignment.rb +0 -74
  128. data/lib/checks/check_model_attributes.rb +0 -36
  129. data/lib/checks/check_redirect.rb +0 -98
  130. data/lib/checks/check_render.rb +0 -65
  131. data/lib/checks/check_send_file.rb +0 -15
  132. data/lib/checks/check_session_settings.rb +0 -79
  133. data/lib/checks/check_sql.rb +0 -146
  134. data/lib/checks/check_validation_regex.rb +0 -60
  135. data/lib/processor.rb +0 -86
  136. data/lib/processors/alias_processor.rb +0 -384
  137. data/lib/processors/controller_alias_processor.rb +0 -237
  138. data/lib/processors/controller_processor.rb +0 -202
  139. data/lib/processors/erubis_template_processor.rb +0 -85
  140. data/lib/processors/lib/find_model_call.rb +0 -39
  141. data/lib/processors/lib/processor_helper.rb +0 -36
  142. data/lib/processors/lib/rails3_route_processor.rb +0 -184
  143. data/lib/processors/lib/route_helper.rb +0 -34
  144. data/lib/processors/params_processor.rb +0 -77
  145. data/lib/processors/route_processor.rb +0 -11
  146. data/lib/processors/template_alias_processor.rb +0 -86
  147. data/lib/report.rb +0 -680
  148. data/lib/scanner.rb +0 -227
  149. data/lib/tracker.rb +0 -144
  150. data/lib/util.rb +0 -141
  151. data/lib/version.rb +0 -1
  152. data/lib/warning.rb +0 -99
@@ -0,0 +1,101 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ #Checks for versions which do not escape single quotes.
4
+ #https://groups.google.com/d/topic/rubyonrails-security/kKGNeMrnmiY/discussion
5
+ class Brakeman::CheckSingleQuotes < Brakeman::BaseCheck
6
+ Brakeman::Checks.add self
7
+ RACK_UTILS = Sexp.new(:colon2, Sexp.new(:const, :Rack), :Utils)
8
+
9
+ @description = "Check for versions which do not escape single quotes (CVE-2012-3464)"
10
+
11
+ def initialize *args
12
+ super
13
+ @inside_erb = @inside_util = @inside_html_escape = @uses_rack_escape = false
14
+ end
15
+
16
+ def run_check
17
+ return if uses_rack_escape?
18
+
19
+ case
20
+ when version_between?('2.0.0', '2.3.14')
21
+ message = "All Rails 2.x versions do not escape single quotes (CVE-2012-3464)"
22
+ when version_between?('3.0.0', '3.0.16')
23
+ message = "Rails #{tracker.config[:rails_version]} does not escape single quotes (CVE-2012-3464). Upgrade to 3.0.17"
24
+ when version_between?('3.1.0', '3.1.7')
25
+ message = "Rails #{tracker.config[:rails_version]} does not escape single quotes (CVE-2012-3464). Upgrade to 3.1.8"
26
+ when version_between?('3.2.0', '3.2.7')
27
+ message = "Rails #{tracker.config[:rails_version]} does not escape single quotes (CVE-2012-3464). Upgrade to 3.2.8"
28
+ else
29
+ return
30
+ end
31
+
32
+ warn :warning_type => "Cross Site Scripting",
33
+ :warning_code => :CVE_2012_3464,
34
+ :message => message,
35
+ :confidence => CONFIDENCE[:med],
36
+ :file => gemfile_or_environment,
37
+ :link_path => "https://groups.google.com/d/topic/rubyonrails-security/kKGNeMrnmiY/discussion"
38
+ end
39
+
40
+ #Process initializers to see if they use workaround
41
+ #by replacing Erb::Util.html_escape
42
+ def uses_rack_escape?
43
+ @tracker.initializers.each do |name, src|
44
+ process src
45
+ end
46
+
47
+ @uses_rack_escape
48
+ end
49
+
50
+ #Look for
51
+ #
52
+ # class ERB
53
+ def process_class exp
54
+ if exp.class_name == :ERB
55
+ @inside_erb = true
56
+ process_all exp.body
57
+ @inside_erb = false
58
+ end
59
+
60
+ exp
61
+ end
62
+
63
+ #Look for
64
+ #
65
+ # module Util
66
+ def process_module exp
67
+ if @inside_erb and exp.module_name == :Util
68
+ @inside_util = true
69
+ process_all exp.body
70
+ @inside_util = false
71
+ end
72
+
73
+ exp
74
+ end
75
+
76
+ #Look for
77
+ #
78
+ # def html_escape
79
+ def process_defn exp
80
+ if @inside_util and exp.method_name == :html_escape
81
+ @inside_html_escape = true
82
+ process_all exp.body
83
+ @inside_html_escape = false
84
+ end
85
+
86
+ exp
87
+ end
88
+
89
+ #Look for
90
+ #
91
+ # Rack::Utils.escape_html
92
+ def process_call exp
93
+ if @inside_html_escape and exp.target == RACK_UTILS and exp.method == :escape_html
94
+ @uses_rack_escape = true
95
+ else
96
+ process exp.target if exp.target
97
+ end
98
+
99
+ exp
100
+ end
101
+ end
@@ -0,0 +1,62 @@
1
+ require 'brakeman/checks/base_check'
2
+
3
+ #At the moment, this looks for
4
+ #
5
+ # skip_before_filter :verify_authenticity_token, :except => [...]
6
+ #
7
+ #which is essentially a blacklist approach (no actions are checked EXCEPT the
8
+ #ones listed) versus a whitelist approach (ONLY the actions listed will skip
9
+ #the check)
10
+ class Brakeman::CheckSkipBeforeFilter < Brakeman::BaseCheck
11
+ Brakeman::Checks.add self
12
+
13
+ @description = "Warn when skipping CSRF or authentication checks by default"
14
+
15
+ def run_check
16
+ tracker.controllers.each do |name, controller|
17
+ filter_skips = (controller[:options][:skip_before_filter] || []) + (controller[:options][:skip_filter] || [])
18
+
19
+ filter_skips.each do |filter|
20
+ process_skip_filter filter, controller
21
+ end
22
+ end
23
+ end
24
+
25
+ def process_skip_filter filter, controller
26
+ case skip_except_value filter
27
+ when :verify_authenticity_token
28
+ warn :class => controller[:name], #ugh this should be a controller warning, too
29
+ :warning_type => "Cross-Site Request Forgery",
30
+ :warning_code => :csrf_blacklist,
31
+ :message => "Use whitelist (:only => [..]) when skipping CSRF check",
32
+ :code => filter,
33
+ :confidence => CONFIDENCE[:med],
34
+ :file => controller[:file]
35
+
36
+ when :login_required, :authenticate_user!, :require_user
37
+ warn :controller => controller[:name],
38
+ :warning_code => :auth_blacklist,
39
+ :warning_type => "Authentication",
40
+ :message => "Use whitelist (:only => [..]) when skipping authentication",
41
+ :code => filter,
42
+ :confidence => CONFIDENCE[:med],
43
+ :link => "authentication_whitelist",
44
+ :file => controller[:file]
45
+ end
46
+ end
47
+
48
+ def skip_except_value filter
49
+ return false unless call? filter
50
+
51
+ first_arg = filter.first_arg
52
+ last_arg = filter.last_arg
53
+
54
+ if symbol? first_arg and hash? last_arg
55
+ if hash_access(last_arg, :except)
56
+ return first_arg.value
57
+ end
58
+ end
59
+
60
+ false
61
+ end
62
+ end
@@ -0,0 +1,577 @@
1
+ require 'brakeman/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 Brakeman::CheckSQL < Brakeman::BaseCheck
12
+ Brakeman::Checks.add self
13
+
14
+ @description = "Check for SQL injection"
15
+
16
+ def run_check
17
+ @rails_version = tracker.config[:rails_version]
18
+
19
+ @sql_targets = [:all, :average, :calculate, :count, :count_by_sql, :exists?,
20
+ :find, :find_by_sql, :first, :last, :maximum, :minimum, :pluck, :sum, :update_all]
21
+ @sql_targets.concat [:from, :group, :having, :joins, :lock, :order, :reorder, :select, :where] if tracker.options[:rails3]
22
+
23
+ Brakeman.debug "Finding possible SQL calls on models"
24
+ calls = tracker.find_call :targets => active_record_models.keys,
25
+ :methods => @sql_targets,
26
+ :chained => true
27
+
28
+ Brakeman.debug "Finding possible SQL calls with no target"
29
+ calls.concat tracker.find_call(:target => nil, :method => @sql_targets)
30
+
31
+ Brakeman.debug "Finding possible SQL calls using constantized()"
32
+ calls.concat tracker.find_call(:method => @sql_targets).select { |result| constantize_call? result }
33
+
34
+ Brakeman.debug "Finding calls to named_scope or scope"
35
+ calls.concat find_scope_calls
36
+
37
+ Brakeman.debug "Checking version of Rails for CVE issues"
38
+ check_rails_versions_against_cve_issues
39
+
40
+ Brakeman.debug "Processing possible SQL calls"
41
+ calls.each { |call| process_result call }
42
+ end
43
+
44
+ #Find calls to named_scope() or scope() in models
45
+ #RP 3 TODO
46
+ def find_scope_calls
47
+ scope_calls = []
48
+
49
+ if version_between?("2.1.0", "3.0.9")
50
+ ar_scope_calls(:named_scope) do |name, args|
51
+ call = make_call(nil, :named_scope, args).line(args.line)
52
+ scope_calls << scope_call_hash(call, name, :named_scope)
53
+ end
54
+ elsif version_between?("3.1.0", "3.9.9")
55
+ ar_scope_calls(:scope) do |name, args|
56
+ second_arg = args[2]
57
+ next unless sexp? second_arg
58
+
59
+ if second_arg.node_type == :iter and node_type? second_arg.block, :block, :call
60
+ process_scope_with_block(name, args)
61
+ elsif second_arg.node_type == :call
62
+ call = second_arg
63
+ scope_calls << scope_call_hash(call, name, call.method)
64
+ else
65
+ call = make_call(nil, :scope, args).line(args.line)
66
+ scope_calls << scope_call_hash(call, name, :scope)
67
+ end
68
+ end
69
+ end
70
+
71
+ scope_calls
72
+ end
73
+
74
+ def ar_scope_calls(symbol_name = :named_scope, &block)
75
+ return_array = []
76
+ active_record_models.each do |name, model|
77
+ model_args = model[:options][symbol_name]
78
+ if model_args
79
+ model_args.each do |args|
80
+ yield name, args
81
+ return_array << [name, args]
82
+ end
83
+ end
84
+ end
85
+ return_array
86
+ end
87
+
88
+ def scope_call_hash(call, name, method)
89
+ { :call => call, :location => { :type => :class, :class => name }, :method => :named_scope }
90
+ end
91
+
92
+
93
+ def process_scope_with_block model_name, args
94
+ scope_name = args[1][1]
95
+ block = args[-1][-1]
96
+
97
+ # Search lambda for calls to query methods
98
+ if block.node_type == :block
99
+ find_calls = Brakeman::FindAllCalls.new(tracker)
100
+ find_calls.process_source(block, :class => model_name, :method => scope_name)
101
+ find_calls.calls.each { |call| process_result(call) if @sql_targets.include?(call[:method]) }
102
+ elsif block.node_type == :call
103
+ process_result :target => block.target, :method => block.method, :call => block,
104
+ :location => { :type => :class, :class => model_name, :method => scope_name }
105
+ end
106
+ end
107
+
108
+ #Process possible SQL injection sites:
109
+ #
110
+ # Model#find
111
+ #
112
+ # Model#(named_)scope
113
+ #
114
+ # Model#(find|count)_by_sql
115
+ #
116
+ # Model#all
117
+ #
118
+ ### Rails 3
119
+ #
120
+ # Model#(where|having)
121
+ # Model#(order|group)
122
+ #
123
+ ### Find Options Hash
124
+ #
125
+ # Dangerous keys that accept SQL:
126
+ #
127
+ # * conditions
128
+ # * order
129
+ # * having
130
+ # * joins
131
+ # * select
132
+ # * from
133
+ # * lock
134
+ #
135
+ def process_result result
136
+ return if duplicate?(result) or result[:call].original_line
137
+
138
+ call = result[:call]
139
+ method = call.method
140
+
141
+ dangerous_value = case method
142
+ when :find
143
+ check_find_arguments call.second_arg
144
+ when :exists?
145
+ check_find_arguments call.first_arg
146
+ when :named_scope, :scope
147
+ check_scope_arguments call
148
+ when :find_by_sql, :count_by_sql
149
+ check_by_sql_arguments call.first_arg
150
+ when :calculate
151
+ check_find_arguments call.third_arg
152
+ when :last, :first, :all
153
+ check_find_arguments call.first_arg
154
+ when :average, :count, :maximum, :minimum, :sum
155
+ if call.length > 5
156
+ unsafe_sql?(call.first_arg) or check_find_arguments(call.last_arg)
157
+ else
158
+ check_find_arguments call.last_arg
159
+ end
160
+ when :where, :having
161
+ check_query_arguments call.arglist
162
+ when :order, :group, :reorder
163
+ check_order_arguments call.arglist
164
+ when :joins
165
+ check_joins_arguments call.first_arg
166
+ when :from, :select
167
+ unsafe_sql? call.first_arg
168
+ when :lock
169
+ check_lock_arguments call.first_arg
170
+ when :pluck
171
+ unsafe_sql? call.first_arg
172
+ when :update_all
173
+ check_update_all_arguments call.args
174
+ else
175
+ Brakeman.debug "Unhandled SQL method: #{method}"
176
+ end
177
+
178
+ if dangerous_value
179
+ add_result result
180
+
181
+ input = include_user_input? dangerous_value
182
+ if input
183
+ confidence = CONFIDENCE[:high]
184
+ user_input = input.match
185
+ else
186
+ confidence = CONFIDENCE[:med]
187
+ user_input = dangerous_value
188
+ end
189
+
190
+ warn :result => result,
191
+ :warning_type => "SQL Injection",
192
+ :warning_code => :sql_injection,
193
+ :message => "Possible SQL injection",
194
+ :user_input => user_input,
195
+ :confidence => confidence
196
+ end
197
+
198
+ if check_for_limit_or_offset_vulnerability call.last_arg
199
+ if include_user_input? call.last_arg
200
+ confidence = CONFIDENCE[:high]
201
+ else
202
+ confidence = CONFIDENCE[:low]
203
+ end
204
+
205
+ warn :result => result,
206
+ :warning_type => "SQL Injection",
207
+ :warning_code => :sql_injection_limit_offset,
208
+ :message => "Upgrade to Rails >= 2.1.2 to escape :limit and :offset. Possible SQL injection",
209
+ :confidence => confidence
210
+ end
211
+ end
212
+
213
+
214
+ #The 'find' methods accept a number of different types of parameters:
215
+ #
216
+ # * The first argument might be :all, :first, or :last
217
+ # * The first argument might be an integer ID or an array of IDs
218
+ # * The second argument might be a hash of options, some of which are
219
+ # dangerous and some of which are not
220
+ # * The second argument might contain SQL fragments as values
221
+ # * The second argument might contain properly parameterized SQL fragments in arrays
222
+ # * The second argument might contain improperly parameterized SQL fragments in arrays
223
+ #
224
+ #This method should only be passed the second argument.
225
+ def check_find_arguments arg
226
+ return nil if not sexp? arg or node_type? arg, :lit, :string, :str, :true, :false, :nil
227
+
228
+ unsafe_sql? arg
229
+ end
230
+
231
+ def check_scope_arguments call
232
+ scope_arg = call.second_arg #first arg is name of scope
233
+
234
+ node_type?(scope_arg, :iter) ? unsafe_sql?(scope_arg.block) : unsafe_sql?(scope_arg)
235
+ end
236
+
237
+ def check_query_arguments arg
238
+ return unless sexp? arg
239
+ first_arg = arg[1]
240
+
241
+ if node_type? arg, :arglist
242
+ if arg.length > 2 and node_type? first_arg, :string_interp, :dstr
243
+ # Model.where("blah = ?", blah)
244
+ return check_string_interp first_arg
245
+ else
246
+ arg = first_arg
247
+ end
248
+ end
249
+
250
+ if request_value? arg
251
+ # Model.where(params[:where])
252
+ arg
253
+ elsif hash? arg
254
+ #This is generally going to be a hash of column names and values, which
255
+ #would escape the values. But the keys _could_ be user input.
256
+ check_hash_keys arg
257
+ elsif node_type? arg, :lit, :str
258
+ nil
259
+ else
260
+ #Hashes are safe...but we check above for hash, so...?
261
+ unsafe_sql? arg, :ignore_hash
262
+ end
263
+ end
264
+
265
+ #Checks each argument to order/reorder/group for possible SQL.
266
+ #Anything used with these methods is passed in verbatim.
267
+ def check_order_arguments args
268
+ return unless sexp? args
269
+
270
+ if node_type? args, :arglist
271
+ check_update_all_arguments(args)
272
+ else
273
+ unsafe_sql? args
274
+ end
275
+ end
276
+
277
+ #find_by_sql and count_by_sql can take either a straight SQL string
278
+ #or an array with values to bind.
279
+ def check_by_sql_arguments arg
280
+ return unless sexp? arg
281
+
282
+ #This is kind of unnecessary, because unsafe_sql? will handle an array
283
+ #correctly, but might be better to be explicit.
284
+ array?(arg) ? unsafe_sql?(arg[1]) : unsafe_sql?(arg)
285
+ end
286
+
287
+ #joins can take a string, hash of associations, or an array of both(?)
288
+ #We only care about the possible string values.
289
+ def check_joins_arguments arg
290
+ return unless sexp? arg and not node_type? arg, :hash, :string, :str
291
+
292
+ if array? arg
293
+ arg.each do |a|
294
+ unsafe_arg = check_joins_arguments a
295
+ return unsafe_arg if unsafe_arg
296
+ end
297
+
298
+ nil
299
+ else
300
+ unsafe_sql? arg
301
+ end
302
+ end
303
+
304
+ def check_update_all_arguments args
305
+ args.each do |arg|
306
+ unsafe_arg = unsafe_sql? arg
307
+ return unsafe_arg if unsafe_arg
308
+ end
309
+
310
+ nil
311
+ end
312
+
313
+ #Model#lock essentially only cares about strings. But those strings can be
314
+ #any SQL fragment. This does not apply to all databases. (For those who do not
315
+ #support it, the lock method does nothing).
316
+ def check_lock_arguments arg
317
+ return unless sexp? arg and not node_type? arg, :hash, :array, :string, :str
318
+
319
+ unsafe_sql?(arg, :ignore_hash)
320
+ end
321
+
322
+
323
+ #Check hash keys for user input.
324
+ #(Seems unlikely, but if a user can control the column names queried, that
325
+ #could be bad)
326
+ def check_hash_keys exp
327
+ hash_iterate(exp) do |key, value|
328
+ unless symbol?(key)
329
+ unsafe_key = unsafe_sql? value
330
+ return unsafe_key if unsafe_key
331
+ end
332
+ end
333
+
334
+ false
335
+ end
336
+
337
+ #Check an interpolated string for dangerous values.
338
+ #
339
+ #This method assumes values interpolated into strings are unsafe by default,
340
+ #unless safe_value? explicitly returns true.
341
+ def check_string_interp arg
342
+ arg.each do |exp|
343
+ return exp.value if node_type?(exp, :string_eval, :evstr) and not safe_value?(exp.value)
344
+ end
345
+
346
+ nil
347
+ end
348
+
349
+ #Checks the given expression for unsafe SQL values. If an unsafe value is
350
+ #found, returns that value (may be the given _exp_ or a subexpression).
351
+ #
352
+ #Otherwise, returns false/nil.
353
+ def unsafe_sql? exp, ignore_hash = false
354
+ return unless sexp?(exp)
355
+
356
+ dangerous_value = find_dangerous_value exp, ignore_hash
357
+ safe_value?(dangerous_value) ? false : dangerous_value
358
+ end
359
+
360
+ #Check _exp_ for dangerous values. Used by unsafe_sql?
361
+ def find_dangerous_value exp, ignore_hash
362
+ case exp.node_type
363
+ when :lit, :str, :const, :colon2, :true, :false, :nil
364
+ nil
365
+ when :array
366
+ #Assume this is an array like
367
+ #
368
+ # ["blah = ? AND thing = ?", ...]
369
+ #
370
+ #and check first value
371
+ unsafe_sql? exp[1]
372
+ when :string_interp, :dstr
373
+ check_string_interp exp
374
+ when :hash
375
+ check_hash_values exp unless ignore_hash
376
+ when :if
377
+ unsafe_sql? exp.then_clause or unsafe_sql? exp.else_clause
378
+ when :call
379
+ unless IGNORE_METHODS_IN_SQL.include? exp.method
380
+ if has_immediate_user_input? exp or has_immediate_model? exp
381
+ exp
382
+ else
383
+ check_call exp
384
+ end
385
+ end
386
+ when :or
387
+ if unsafe = (unsafe_sql?(exp.lhs) || unsafe_sql?(exp.rhs))
388
+ unsafe
389
+ else
390
+ nil
391
+ end
392
+ when :block, :rlist
393
+ unsafe_sql? exp.last
394
+ else
395
+ if has_immediate_user_input? exp or has_immediate_model? exp
396
+ exp
397
+ else
398
+ nil
399
+ end
400
+ end
401
+ end
402
+
403
+ #Checks hash values associated with these keys:
404
+ #
405
+ # * conditions
406
+ # * order
407
+ # * having
408
+ # * joins
409
+ # * select
410
+ # * from
411
+ # * lock
412
+ def check_hash_values exp
413
+ hash_iterate(exp) do |key, value|
414
+ if symbol? key
415
+ unsafe = case key.value
416
+ when :conditions, :having, :select
417
+ check_query_arguments value
418
+ when :order, :group
419
+ check_order_arguments value
420
+ when :joins
421
+ check_joins_arguments value
422
+ when :lock
423
+ check_lock_arguments value
424
+ when :from
425
+ unsafe_sql? value
426
+ else
427
+ nil
428
+ end
429
+
430
+ return unsafe if unsafe
431
+ end
432
+ end
433
+
434
+ false
435
+ end
436
+
437
+ STRING_METHODS = Set[:<<, :+, :concat, :prepend]
438
+
439
+ def check_for_string_building exp
440
+ return unless call? exp
441
+
442
+ target = exp.target
443
+ method = exp.method
444
+
445
+ if string? target or string? exp.first_arg
446
+ return exp if STRING_METHODS.include? method
447
+ elsif STRING_METHODS.include? method and call? target
448
+ return unsafe_sql? target
449
+ end
450
+
451
+ nil
452
+ end
453
+
454
+ IGNORE_METHODS_IN_SQL = Set[:id, :merge_conditions, :table_name, :to_i, :to_f,
455
+ :sanitize_sql, :sanitize_sql_array, :sanitize_sql_for_assignment,
456
+ :sanitize_sql_for_conditions, :sanitize_sql_hash,
457
+ :sanitize_sql_hash_for_assignment, :sanitize_sql_hash_for_conditions,
458
+ :to_sql]
459
+
460
+ def safe_value? exp
461
+ return true unless sexp? exp
462
+
463
+ case exp.node_type
464
+ when :str, :lit, :const, :colon2, :nil, :true, :false
465
+ true
466
+ when :call
467
+ IGNORE_METHODS_IN_SQL.include? exp.method
468
+ when :if
469
+ safe_value? exp.then_clause and safe_value? exp.else_clause
470
+ when :block, :rlist
471
+ safe_value? exp.last
472
+ when :or
473
+ safe_value? exp.lhs and safe_value? exp.rhs
474
+ else
475
+ false
476
+ end
477
+ end
478
+
479
+ #Check call for string building
480
+ def check_call exp
481
+ return unless call? exp
482
+ unsafe = check_for_string_building exp
483
+
484
+ if unsafe
485
+ unsafe
486
+ elsif call? exp.target
487
+ check_call exp.target
488
+ else
489
+ nil
490
+ end
491
+ end
492
+
493
+ #Prior to Rails 2.1.1, the :offset and :limit parameters were not
494
+ #escaping input properly.
495
+ #
496
+ #http://www.rorsecurity.info/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter/
497
+ def check_for_limit_or_offset_vulnerability options
498
+ return false if @rails_version.nil? or @rails_version >= "2.1.1" or not hash?(options)
499
+
500
+ return true if hash_access(options, :limit) or hash_access(options, :offset)
501
+
502
+ false
503
+ end
504
+
505
+ #Look for something like this:
506
+ #
507
+ # params[:x].constantize.find('something')
508
+ #
509
+ # s(:call,
510
+ # s(:call,
511
+ # s(:call,
512
+ # s(:call, nil, :params, s(:arglist)),
513
+ # :[],
514
+ # s(:arglist, s(:lit, :x))),
515
+ # :constantize,
516
+ # s(:arglist)),
517
+ # :find,
518
+ # s(:arglist, s(:str, "something")))
519
+ def constantize_call? result
520
+ call = result[:call]
521
+ call? call.target and call.target.method == :constantize
522
+ end
523
+
524
+ def upgrade_version? versions
525
+ versions.each do |low, high, upgrade|
526
+ return upgrade if version_between? low, high
527
+ end
528
+
529
+ false
530
+ end
531
+
532
+ def check_rails_versions_against_cve_issues
533
+ [
534
+ {
535
+ :cve => "CVE-2012-2660",
536
+ :versions => [%w[2.0.0 2.3.14 2.3.17], %w[3.0.0 3.0.12 3.0.13], %w[3.1.0 3.1.4 3.1.5], %w[3.2.0 3.2.3 3.2.4]],
537
+ :url => "https://groups.google.com/d/topic/rubyonrails-security/8SA-M3as7A8/discussion"
538
+ },
539
+ {
540
+ :cve => "CVE-2012-2661",
541
+ :versions => [%w[3.0.0 3.0.12 3.0.13], %w[3.1.0 3.1.4 3.1.5], %w[3.2.0 3.2.3 3.2.5]],
542
+ :url => "https://groups.google.com/d/topic/rubyonrails-security/dUaiOOGWL1k/discussion"
543
+ },
544
+ {
545
+ :cve => "CVE-2012-2695",
546
+ :versions => [%w[2.0.0 2.3.14 2.3.15], %w[3.0.0 3.0.13 3.0.14], %w[3.1.0 3.1.5 3.1.6], %w[3.2.0 3.2.5 3.2.6]],
547
+ :url => "https://groups.google.com/d/topic/rubyonrails-security/l4L0TEVAz1k/discussion"
548
+ },
549
+ {
550
+ :cve => "CVE-2012-5664",
551
+ :versions => [%w[2.0.0 2.3.14 2.3.15], %w[3.0.0 3.0.17 3.0.18], %w[3.1.0 3.1.8 3.1.9], %w[3.2.0 3.2.9 3.2.18]],
552
+ :url => "https://groups.google.com/d/topic/rubyonrails-security/DCNTNp_qjFM/discussion"
553
+ },
554
+ {
555
+ :cve => "CVE-2013-0155",
556
+ :versions => [%w[2.0.0 2.3.15 2.3.16], %w[3.0.0 3.0.18 3.0.19], %w[3.1.0 3.1.9 3.1.10], %w[3.2.0 3.2.10 3.2.11]],
557
+ :url => "https://groups.google.com/d/topic/rubyonrails-security/c7jT-EeN9eI/discussion"
558
+ },
559
+ ].each do |cve_issue|
560
+ cve_warning_for cve_issue[:versions], cve_issue[:cve], cve_issue[:url]
561
+ end
562
+ end
563
+
564
+ def cve_warning_for versions, cve, link
565
+ upgrade_version = upgrade_version? versions
566
+ return unless upgrade_version
567
+
568
+ code = cve.tr('-', '_').to_sym
569
+
570
+ warn :warning_type => 'SQL Injection',
571
+ :warning_code => code,
572
+ :message => "Rails #{tracker.config[:rails_version]} contains a SQL injection vulnerability (#{cve}). Upgrade to #{upgrade_version}",
573
+ :confidence => CONFIDENCE[:high],
574
+ :file => gemfile_or_environment,
575
+ :link_path => link
576
+ end
577
+ end