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.
- checksums.yaml +7 -0
- data/CHANGES.md +1091 -0
- data/FEATURES +16 -0
- data/README.md +174 -0
- data/bin/railroader +8 -0
- data/lib/railroader/app_tree.rb +191 -0
- data/lib/railroader/call_index.rb +219 -0
- data/lib/railroader/checks/base_check.rb +505 -0
- data/lib/railroader/checks/check_basic_auth.rb +88 -0
- data/lib/railroader/checks/check_basic_auth_timing_attack.rb +33 -0
- data/lib/railroader/checks/check_content_tag.rb +200 -0
- data/lib/railroader/checks/check_create_with.rb +74 -0
- data/lib/railroader/checks/check_cross_site_scripting.rb +381 -0
- data/lib/railroader/checks/check_default_routes.rb +86 -0
- data/lib/railroader/checks/check_deserialize.rb +56 -0
- data/lib/railroader/checks/check_detailed_exceptions.rb +55 -0
- data/lib/railroader/checks/check_digest_dos.rb +38 -0
- data/lib/railroader/checks/check_divide_by_zero.rb +42 -0
- data/lib/railroader/checks/check_dynamic_finders.rb +48 -0
- data/lib/railroader/checks/check_escape_function.rb +21 -0
- data/lib/railroader/checks/check_evaluation.rb +35 -0
- data/lib/railroader/checks/check_execute.rb +189 -0
- data/lib/railroader/checks/check_file_access.rb +71 -0
- data/lib/railroader/checks/check_file_disclosure.rb +35 -0
- data/lib/railroader/checks/check_filter_skipping.rb +31 -0
- data/lib/railroader/checks/check_forgery_setting.rb +81 -0
- data/lib/railroader/checks/check_header_dos.rb +31 -0
- data/lib/railroader/checks/check_i18n_xss.rb +48 -0
- data/lib/railroader/checks/check_jruby_xml.rb +36 -0
- data/lib/railroader/checks/check_json_encoding.rb +47 -0
- data/lib/railroader/checks/check_json_parsing.rb +107 -0
- data/lib/railroader/checks/check_link_to.rb +132 -0
- data/lib/railroader/checks/check_link_to_href.rb +146 -0
- data/lib/railroader/checks/check_mail_to.rb +49 -0
- data/lib/railroader/checks/check_mass_assignment.rb +196 -0
- data/lib/railroader/checks/check_mime_type_dos.rb +39 -0
- data/lib/railroader/checks/check_model_attr_accessible.rb +55 -0
- data/lib/railroader/checks/check_model_attributes.rb +119 -0
- data/lib/railroader/checks/check_model_serialize.rb +67 -0
- data/lib/railroader/checks/check_nested_attributes.rb +38 -0
- data/lib/railroader/checks/check_nested_attributes_bypass.rb +58 -0
- data/lib/railroader/checks/check_number_to_currency.rb +74 -0
- data/lib/railroader/checks/check_permit_attributes.rb +43 -0
- data/lib/railroader/checks/check_quote_table_name.rb +40 -0
- data/lib/railroader/checks/check_redirect.rb +256 -0
- data/lib/railroader/checks/check_regex_dos.rb +68 -0
- data/lib/railroader/checks/check_render.rb +97 -0
- data/lib/railroader/checks/check_render_dos.rb +37 -0
- data/lib/railroader/checks/check_render_inline.rb +53 -0
- data/lib/railroader/checks/check_response_splitting.rb +21 -0
- data/lib/railroader/checks/check_route_dos.rb +42 -0
- data/lib/railroader/checks/check_safe_buffer_manipulation.rb +31 -0
- data/lib/railroader/checks/check_sanitize_methods.rb +112 -0
- data/lib/railroader/checks/check_secrets.rb +40 -0
- data/lib/railroader/checks/check_select_tag.rb +59 -0
- data/lib/railroader/checks/check_select_vulnerability.rb +60 -0
- data/lib/railroader/checks/check_send.rb +47 -0
- data/lib/railroader/checks/check_send_file.rb +19 -0
- data/lib/railroader/checks/check_session_manipulation.rb +35 -0
- data/lib/railroader/checks/check_session_settings.rb +176 -0
- data/lib/railroader/checks/check_simple_format.rb +58 -0
- data/lib/railroader/checks/check_single_quotes.rb +101 -0
- data/lib/railroader/checks/check_skip_before_filter.rb +60 -0
- data/lib/railroader/checks/check_sql.rb +700 -0
- data/lib/railroader/checks/check_sql_cves.rb +106 -0
- data/lib/railroader/checks/check_ssl_verify.rb +48 -0
- data/lib/railroader/checks/check_strip_tags.rb +89 -0
- data/lib/railroader/checks/check_symbol_dos.rb +71 -0
- data/lib/railroader/checks/check_symbol_dos_cve.rb +30 -0
- data/lib/railroader/checks/check_translate_bug.rb +45 -0
- data/lib/railroader/checks/check_unsafe_reflection.rb +50 -0
- data/lib/railroader/checks/check_unscoped_find.rb +57 -0
- data/lib/railroader/checks/check_validation_regex.rb +116 -0
- data/lib/railroader/checks/check_weak_hash.rb +148 -0
- data/lib/railroader/checks/check_without_protection.rb +80 -0
- data/lib/railroader/checks/check_xml_dos.rb +45 -0
- data/lib/railroader/checks/check_yaml_parsing.rb +121 -0
- data/lib/railroader/checks.rb +209 -0
- data/lib/railroader/codeclimate/engine_configuration.rb +97 -0
- data/lib/railroader/commandline.rb +179 -0
- data/lib/railroader/differ.rb +66 -0
- data/lib/railroader/file_parser.rb +54 -0
- data/lib/railroader/format/style.css +133 -0
- data/lib/railroader/options.rb +339 -0
- data/lib/railroader/parsers/rails2_erubis.rb +6 -0
- data/lib/railroader/parsers/rails2_xss_plugin_erubis.rb +48 -0
- data/lib/railroader/parsers/rails3_erubis.rb +81 -0
- data/lib/railroader/parsers/template_parser.rb +108 -0
- data/lib/railroader/processor.rb +102 -0
- data/lib/railroader/processors/alias_processor.rb +1229 -0
- data/lib/railroader/processors/base_processor.rb +295 -0
- data/lib/railroader/processors/config_processor.rb +14 -0
- data/lib/railroader/processors/controller_alias_processor.rb +278 -0
- data/lib/railroader/processors/controller_processor.rb +249 -0
- data/lib/railroader/processors/erb_template_processor.rb +77 -0
- data/lib/railroader/processors/erubis_template_processor.rb +92 -0
- data/lib/railroader/processors/gem_processor.rb +64 -0
- data/lib/railroader/processors/haml_template_processor.rb +191 -0
- data/lib/railroader/processors/lib/basic_processor.rb +37 -0
- data/lib/railroader/processors/lib/call_conversion_helper.rb +90 -0
- data/lib/railroader/processors/lib/find_all_calls.rb +224 -0
- data/lib/railroader/processors/lib/find_call.rb +183 -0
- data/lib/railroader/processors/lib/find_return_value.rb +166 -0
- data/lib/railroader/processors/lib/module_helper.rb +111 -0
- data/lib/railroader/processors/lib/processor_helper.rb +88 -0
- data/lib/railroader/processors/lib/rails2_config_processor.rb +145 -0
- data/lib/railroader/processors/lib/rails2_route_processor.rb +313 -0
- data/lib/railroader/processors/lib/rails3_config_processor.rb +132 -0
- data/lib/railroader/processors/lib/rails3_route_processor.rb +308 -0
- data/lib/railroader/processors/lib/render_helper.rb +181 -0
- data/lib/railroader/processors/lib/render_path.rb +107 -0
- data/lib/railroader/processors/lib/route_helper.rb +68 -0
- data/lib/railroader/processors/lib/safe_call_helper.rb +16 -0
- data/lib/railroader/processors/library_processor.rb +74 -0
- data/lib/railroader/processors/model_processor.rb +91 -0
- data/lib/railroader/processors/output_processor.rb +144 -0
- data/lib/railroader/processors/route_processor.rb +17 -0
- data/lib/railroader/processors/slim_template_processor.rb +111 -0
- data/lib/railroader/processors/template_alias_processor.rb +118 -0
- data/lib/railroader/processors/template_processor.rb +85 -0
- data/lib/railroader/report/config/remediation.yml +71 -0
- data/lib/railroader/report/ignore/config.rb +153 -0
- data/lib/railroader/report/ignore/interactive.rb +362 -0
- data/lib/railroader/report/pager.rb +112 -0
- data/lib/railroader/report/renderer.rb +24 -0
- data/lib/railroader/report/report_base.rb +292 -0
- data/lib/railroader/report/report_codeclimate.rb +79 -0
- data/lib/railroader/report/report_csv.rb +55 -0
- data/lib/railroader/report/report_hash.rb +23 -0
- data/lib/railroader/report/report_html.rb +216 -0
- data/lib/railroader/report/report_json.rb +45 -0
- data/lib/railroader/report/report_markdown.rb +107 -0
- data/lib/railroader/report/report_table.rb +117 -0
- data/lib/railroader/report/report_tabs.rb +17 -0
- data/lib/railroader/report/report_text.rb +198 -0
- data/lib/railroader/report/templates/controller_overview.html.erb +22 -0
- data/lib/railroader/report/templates/controller_warnings.html.erb +21 -0
- data/lib/railroader/report/templates/error_overview.html.erb +29 -0
- data/lib/railroader/report/templates/header.html.erb +58 -0
- data/lib/railroader/report/templates/ignored_warnings.html.erb +25 -0
- data/lib/railroader/report/templates/model_warnings.html.erb +21 -0
- data/lib/railroader/report/templates/overview.html.erb +38 -0
- data/lib/railroader/report/templates/security_warnings.html.erb +23 -0
- data/lib/railroader/report/templates/template_overview.html.erb +21 -0
- data/lib/railroader/report/templates/view_warnings.html.erb +34 -0
- data/lib/railroader/report/templates/warning_overview.html.erb +17 -0
- data/lib/railroader/report.rb +88 -0
- data/lib/railroader/rescanner.rb +483 -0
- data/lib/railroader/scanner.rb +321 -0
- data/lib/railroader/tracker/collection.rb +93 -0
- data/lib/railroader/tracker/config.rb +154 -0
- data/lib/railroader/tracker/constants.rb +171 -0
- data/lib/railroader/tracker/controller.rb +161 -0
- data/lib/railroader/tracker/library.rb +17 -0
- data/lib/railroader/tracker/model.rb +90 -0
- data/lib/railroader/tracker/template.rb +33 -0
- data/lib/railroader/tracker.rb +362 -0
- data/lib/railroader/util.rb +503 -0
- data/lib/railroader/version.rb +3 -0
- data/lib/railroader/warning.rb +294 -0
- data/lib/railroader/warning_codes.rb +117 -0
- data/lib/railroader.rb +544 -0
- data/lib/ruby_parser/bm_sexp.rb +626 -0
- data/lib/ruby_parser/bm_sexp_processor.rb +116 -0
- 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
|