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,505 @@
|
|
|
1
|
+
require 'railroader/processors/output_processor'
|
|
2
|
+
require 'railroader/processors/lib/processor_helper'
|
|
3
|
+
require 'railroader/warning'
|
|
4
|
+
require 'railroader/util'
|
|
5
|
+
|
|
6
|
+
#Basis of vulnerability checks.
|
|
7
|
+
class Railroader::BaseCheck < Railroader::SexpProcessor
|
|
8
|
+
include Railroader::ProcessorHelper
|
|
9
|
+
include Railroader::SafeCallHelper
|
|
10
|
+
include Railroader::Util
|
|
11
|
+
attr_reader :tracker, :warnings
|
|
12
|
+
|
|
13
|
+
# This is for legacy support.
|
|
14
|
+
# Use :high, :medium, or :low instead when creating warnings.
|
|
15
|
+
CONFIDENCE = Railroader::Warning::CONFIDENCE
|
|
16
|
+
|
|
17
|
+
Match = Struct.new(:type, :match)
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :name
|
|
21
|
+
|
|
22
|
+
def inherited(subclass)
|
|
23
|
+
subclass.name = subclass.to_s.match(/^Railroader::(.*)$/)[1]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#Initialize Check with Checks.
|
|
28
|
+
def initialize(app_tree, tracker)
|
|
29
|
+
super()
|
|
30
|
+
@app_tree = app_tree
|
|
31
|
+
@results = [] #only to check for duplicates
|
|
32
|
+
@warnings = []
|
|
33
|
+
@tracker = tracker
|
|
34
|
+
@string_interp = false
|
|
35
|
+
@current_set = nil
|
|
36
|
+
@current_template = @current_module = @current_class = @current_method = nil
|
|
37
|
+
@active_record_models = nil
|
|
38
|
+
@mass_assign_disabled = nil
|
|
39
|
+
@has_user_input = nil
|
|
40
|
+
@safe_input_attributes = Set[:to_i, :to_f, :arel_table, :id]
|
|
41
|
+
@comparison_ops = Set[:==, :!=, :>, :<, :>=, :<=]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#Add result to result list, which is used to check for duplicates
|
|
45
|
+
def add_result result, location = nil
|
|
46
|
+
location ||= (@current_template && @current_template.name) || @current_class || @current_module || @current_set || result[:location][:class] || result[:location][:template]
|
|
47
|
+
location = location[:name] if location.is_a? Hash
|
|
48
|
+
location = location.name if location.is_a? Railroader::Collection
|
|
49
|
+
location = location.to_sym
|
|
50
|
+
|
|
51
|
+
if result.is_a? Hash
|
|
52
|
+
line = result[:call].original_line || result[:call].line
|
|
53
|
+
elsif sexp? result
|
|
54
|
+
line = result.original_line || result.line
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@results << [line, location, result]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#Default Sexp processing. Iterates over each value in the Sexp
|
|
63
|
+
#and processes them if they are also Sexps.
|
|
64
|
+
def process_default exp
|
|
65
|
+
exp.each do |e|
|
|
66
|
+
process e if sexp? e
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
exp
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#Process calls and check if they include user input
|
|
73
|
+
def process_call exp
|
|
74
|
+
unless @comparison_ops.include? exp.method
|
|
75
|
+
process exp.target if sexp? exp.target
|
|
76
|
+
process_call_args exp
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
target = exp.target
|
|
80
|
+
|
|
81
|
+
unless always_safe_method? exp.method
|
|
82
|
+
if params? target
|
|
83
|
+
@has_user_input = Match.new(:params, exp)
|
|
84
|
+
elsif cookies? target
|
|
85
|
+
@has_user_input = Match.new(:cookies, exp)
|
|
86
|
+
elsif request_env? target
|
|
87
|
+
@has_user_input = Match.new(:request, exp)
|
|
88
|
+
elsif sexp? target and model_name? target[1] #TODO: Can this be target.target?
|
|
89
|
+
@has_user_input = Match.new(:model, exp)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
exp
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def process_if exp
|
|
97
|
+
#This is to ignore user input in condition
|
|
98
|
+
current_user_input = @has_user_input
|
|
99
|
+
process exp.condition
|
|
100
|
+
@has_user_input = current_user_input
|
|
101
|
+
|
|
102
|
+
process exp.then_clause if sexp? exp.then_clause
|
|
103
|
+
process exp.else_clause if sexp? exp.else_clause
|
|
104
|
+
|
|
105
|
+
exp
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#Note that params are included in current expression
|
|
109
|
+
def process_params exp
|
|
110
|
+
@has_user_input = Match.new(:params, exp)
|
|
111
|
+
exp
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#Note that cookies are included in current expression
|
|
115
|
+
def process_cookies exp
|
|
116
|
+
@has_user_input = Match.new(:cookies, exp)
|
|
117
|
+
exp
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
#Does not actually process string interpolation, but notes that it occurred.
|
|
121
|
+
def process_dstr exp
|
|
122
|
+
unless @string_interp # don't overwrite existing value
|
|
123
|
+
@string_interp = Match.new(:interp, exp)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
process_default exp
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def always_safe_method? meth
|
|
132
|
+
@safe_input_attributes.include? meth or
|
|
133
|
+
@comparison_ops.include? meth
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def boolean_method? method
|
|
137
|
+
method[-1] == "?"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#Report a warning
|
|
141
|
+
def warn options
|
|
142
|
+
extra_opts = { :check => self.class.to_s }
|
|
143
|
+
|
|
144
|
+
warning = Railroader::Warning.new(options.merge(extra_opts))
|
|
145
|
+
warning.file = file_for warning
|
|
146
|
+
warning.relative_path = relative_path(warning.file)
|
|
147
|
+
|
|
148
|
+
@warnings << warning
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
#Run _exp_ through OutputProcessor to get a nice String.
|
|
152
|
+
def format_output exp
|
|
153
|
+
Railroader::OutputProcessor.new.format(exp).gsub(/\r|\n/, "")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
#Checks if mass assignment is disabled globally in an initializer.
|
|
157
|
+
def mass_assign_disabled?
|
|
158
|
+
return @mass_assign_disabled unless @mass_assign_disabled.nil?
|
|
159
|
+
|
|
160
|
+
@mass_assign_disabled = false
|
|
161
|
+
|
|
162
|
+
if version_between?("3.1.0", "3.9.9") and
|
|
163
|
+
tracker.config.whitelist_attributes?
|
|
164
|
+
|
|
165
|
+
@mass_assign_disabled = true
|
|
166
|
+
elsif tracker.options[:rails4] && (!tracker.config.has_gem?(:protected_attributes) || tracker.config.whitelist_attributes?)
|
|
167
|
+
|
|
168
|
+
@mass_assign_disabled = true
|
|
169
|
+
else
|
|
170
|
+
#Check for ActiveRecord::Base.send(:attr_accessible, nil)
|
|
171
|
+
tracker.check_initializers(:"ActiveRecord::Base", :attr_accessible).each do |result|
|
|
172
|
+
call = result.call
|
|
173
|
+
if call? call
|
|
174
|
+
if call.first_arg == Sexp.new(:nil)
|
|
175
|
+
@mass_assign_disabled = true
|
|
176
|
+
break
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
unless @mass_assign_disabled
|
|
182
|
+
tracker.check_initializers(:"ActiveRecord::Base", :send).each do |result|
|
|
183
|
+
call = result.call
|
|
184
|
+
if call? call
|
|
185
|
+
if call.first_arg == Sexp.new(:lit, :attr_accessible) and call.second_arg == Sexp.new(:nil)
|
|
186
|
+
@mass_assign_disabled = true
|
|
187
|
+
break
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
unless @mass_assign_disabled
|
|
194
|
+
#Check for
|
|
195
|
+
# class ActiveRecord::Base
|
|
196
|
+
# attr_accessible nil
|
|
197
|
+
# end
|
|
198
|
+
matches = tracker.check_initializers([], :attr_accessible)
|
|
199
|
+
|
|
200
|
+
matches.each do |result|
|
|
201
|
+
if result.module == "ActiveRecord" and result.result_class == :Base
|
|
202
|
+
arg = result.call.first_arg
|
|
203
|
+
|
|
204
|
+
if arg.nil? or node_type? arg, :nil
|
|
205
|
+
@mass_assign_disabled = true
|
|
206
|
+
break
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
#There is a chance someone is using Rails 3.x and the `strong_parameters`
|
|
214
|
+
#gem and still using hack above, so this is a separate check for
|
|
215
|
+
#including ActiveModel::ForbiddenAttributesProtection in
|
|
216
|
+
#ActiveRecord::Base in an initializer.
|
|
217
|
+
if not @mass_assign_disabled and version_between?("3.1.0", "3.9.9") and tracker.config.has_gem? :strong_parameters
|
|
218
|
+
matches = tracker.check_initializers([], :include)
|
|
219
|
+
forbidden_protection = Sexp.new(:colon2, Sexp.new(:const, :ActiveModel), :ForbiddenAttributesProtection)
|
|
220
|
+
|
|
221
|
+
matches.each do |result|
|
|
222
|
+
if call? result.call and result.call.first_arg == forbidden_protection
|
|
223
|
+
@mass_assign_disabled = true
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
unless @mass_assign_disabled
|
|
228
|
+
matches = tracker.check_initializers(:"ActiveRecord::Base", [:send, :include])
|
|
229
|
+
|
|
230
|
+
matches.each do |result|
|
|
231
|
+
call = result.call
|
|
232
|
+
if call? call and (call.first_arg == forbidden_protection or call.second_arg == forbidden_protection)
|
|
233
|
+
@mass_assign_disabled = true
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
@mass_assign_disabled
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def original? result
|
|
243
|
+
return false if result[:call].original_line or duplicate? result
|
|
244
|
+
add_result result
|
|
245
|
+
true
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
#This is to avoid reporting duplicates. Checks if the result has been
|
|
249
|
+
#reported already from the same line number.
|
|
250
|
+
def duplicate? result, location = nil
|
|
251
|
+
if result.is_a? Hash
|
|
252
|
+
line = result[:call].original_line || result[:call].line
|
|
253
|
+
elsif sexp? result
|
|
254
|
+
line = result.original_line || result.line
|
|
255
|
+
else
|
|
256
|
+
raise ArgumentError
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
location ||= (@current_template && @current_template.name) || @current_class || @current_module || @current_set || result[:location][:class] || result[:location][:template]
|
|
260
|
+
|
|
261
|
+
location = location[:name] if location.is_a? Hash
|
|
262
|
+
location = location.name if location.is_a? Railroader::Collection
|
|
263
|
+
location = location.to_sym
|
|
264
|
+
|
|
265
|
+
@results.each do |r|
|
|
266
|
+
if r[0] == line and r[1] == location
|
|
267
|
+
if tracker.options[:combine_locations]
|
|
268
|
+
return true
|
|
269
|
+
elsif r[2] == result
|
|
270
|
+
return true
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
false
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
#Checks if an expression contains string interpolation.
|
|
279
|
+
#
|
|
280
|
+
#Returns Match with :interp type if found.
|
|
281
|
+
def include_interp? exp
|
|
282
|
+
@string_interp = false
|
|
283
|
+
process exp
|
|
284
|
+
@string_interp
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
#Checks if _exp_ includes user input in the form of cookies, parameters,
|
|
288
|
+
#request environment, or model attributes.
|
|
289
|
+
#
|
|
290
|
+
#If found, returns a struct containing a type (:cookies, :params, :request, :model) and
|
|
291
|
+
#the matching expression (Match#type and Match#match).
|
|
292
|
+
#
|
|
293
|
+
#Returns false otherwise.
|
|
294
|
+
def include_user_input? exp
|
|
295
|
+
@has_user_input = false
|
|
296
|
+
process exp
|
|
297
|
+
@has_user_input
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
#This is used to check for user input being used directly.
|
|
301
|
+
#
|
|
302
|
+
##If found, returns a struct containing a type (:cookies, :params, :request) and
|
|
303
|
+
#the matching expression (Match#type and Match#match).
|
|
304
|
+
#
|
|
305
|
+
#Returns false otherwise.
|
|
306
|
+
def has_immediate_user_input? exp
|
|
307
|
+
if exp.nil?
|
|
308
|
+
false
|
|
309
|
+
elsif call? exp and not always_safe_method? exp.method
|
|
310
|
+
if params? exp
|
|
311
|
+
return Match.new(:params, exp)
|
|
312
|
+
elsif cookies? exp
|
|
313
|
+
return Match.new(:cookies, exp)
|
|
314
|
+
elsif request_env? exp
|
|
315
|
+
return Match.new(:request, exp)
|
|
316
|
+
else
|
|
317
|
+
has_immediate_user_input? exp.target
|
|
318
|
+
end
|
|
319
|
+
elsif sexp? exp
|
|
320
|
+
case exp.node_type
|
|
321
|
+
when :dstr
|
|
322
|
+
exp.each do |e|
|
|
323
|
+
if sexp? e
|
|
324
|
+
match = has_immediate_user_input?(e)
|
|
325
|
+
return match if match
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
false
|
|
329
|
+
when :evstr
|
|
330
|
+
if sexp? exp.value
|
|
331
|
+
if exp.value.node_type == :rlist
|
|
332
|
+
exp.value.each_sexp do |e|
|
|
333
|
+
match = has_immediate_user_input?(e)
|
|
334
|
+
return match if match
|
|
335
|
+
end
|
|
336
|
+
false
|
|
337
|
+
else
|
|
338
|
+
has_immediate_user_input? exp.value
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
when :format
|
|
342
|
+
has_immediate_user_input? exp.value
|
|
343
|
+
when :if
|
|
344
|
+
(sexp? exp.then_clause and has_immediate_user_input? exp.then_clause) or
|
|
345
|
+
(sexp? exp.else_clause and has_immediate_user_input? exp.else_clause)
|
|
346
|
+
when :or
|
|
347
|
+
has_immediate_user_input? exp.lhs or
|
|
348
|
+
has_immediate_user_input? exp.rhs
|
|
349
|
+
else
|
|
350
|
+
false
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
#Checks for a model attribute at the top level of the
|
|
356
|
+
#expression.
|
|
357
|
+
def has_immediate_model? exp, out = nil
|
|
358
|
+
out = exp if out.nil?
|
|
359
|
+
|
|
360
|
+
if sexp? exp and exp.node_type == :output
|
|
361
|
+
exp = exp.value
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
if call? exp
|
|
365
|
+
target = exp.target
|
|
366
|
+
method = exp.method
|
|
367
|
+
|
|
368
|
+
if always_safe_method? method
|
|
369
|
+
false
|
|
370
|
+
elsif call? target and not method.to_s[-1,1] == "?"
|
|
371
|
+
if has_immediate_model?(target, out)
|
|
372
|
+
exp
|
|
373
|
+
else
|
|
374
|
+
false
|
|
375
|
+
end
|
|
376
|
+
elsif model_name? target
|
|
377
|
+
exp
|
|
378
|
+
else
|
|
379
|
+
false
|
|
380
|
+
end
|
|
381
|
+
elsif sexp? exp
|
|
382
|
+
case exp.node_type
|
|
383
|
+
when :dstr
|
|
384
|
+
exp.each do |e|
|
|
385
|
+
if sexp? e and match = has_immediate_model?(e, out)
|
|
386
|
+
return match
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
false
|
|
390
|
+
when :evstr
|
|
391
|
+
if sexp? exp.value
|
|
392
|
+
if exp.value.node_type == :rlist
|
|
393
|
+
exp.value.each_sexp do |e|
|
|
394
|
+
if match = has_immediate_model?(e, out)
|
|
395
|
+
return match
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
false
|
|
399
|
+
else
|
|
400
|
+
has_immediate_model? exp.value, out
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
when :format
|
|
404
|
+
has_immediate_model? exp.value, out
|
|
405
|
+
when :if
|
|
406
|
+
((sexp? exp.then_clause and has_immediate_model? exp.then_clause, out) or
|
|
407
|
+
(sexp? exp.else_clause and has_immediate_model? exp.else_clause, out))
|
|
408
|
+
when :or
|
|
409
|
+
has_immediate_model? exp.lhs or
|
|
410
|
+
has_immediate_model? exp.rhs
|
|
411
|
+
else
|
|
412
|
+
false
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
#Checks if +exp+ is a model name.
|
|
418
|
+
#
|
|
419
|
+
#Prior to using this method, either @tracker must be set to
|
|
420
|
+
#the current tracker, or else @models should contain an array of the model
|
|
421
|
+
#names, which is available via tracker.models.keys
|
|
422
|
+
def model_name? exp
|
|
423
|
+
@models ||= @tracker.models.keys
|
|
424
|
+
|
|
425
|
+
if exp.is_a? Symbol
|
|
426
|
+
@models.include? exp
|
|
427
|
+
elsif call? exp and exp.target.nil? and exp.method == :current_user
|
|
428
|
+
true
|
|
429
|
+
elsif sexp? exp
|
|
430
|
+
@models.include? class_name(exp)
|
|
431
|
+
else
|
|
432
|
+
false
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
#Returns true if +target+ is in +exp+
|
|
437
|
+
def include_target? exp, target
|
|
438
|
+
return false unless call? exp
|
|
439
|
+
|
|
440
|
+
exp.each do |e|
|
|
441
|
+
return true if e == target or include_target? e, target
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
false
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def lts_version? version
|
|
448
|
+
tracker.config.has_gem? :'railslts-version' and
|
|
449
|
+
version_between? version, "2.3.18.99", tracker.config.gem_version(:'railslts-version')
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def version_between? low_version, high_version, current_version = nil
|
|
454
|
+
tracker.config.version_between? low_version, high_version, current_version
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def gemfile_or_environment gem_name = :rails
|
|
458
|
+
if gem_name and info = tracker.config.get_gem(gem_name)
|
|
459
|
+
info
|
|
460
|
+
elsif @app_tree.exists?("Gemfile")
|
|
461
|
+
"Gemfile"
|
|
462
|
+
elsif @app_tree.exists?("gems.rb")
|
|
463
|
+
"gems.rb"
|
|
464
|
+
else
|
|
465
|
+
"config/environment.rb"
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def self.description
|
|
470
|
+
@description
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def active_record_models
|
|
474
|
+
return @active_record_models if @active_record_models
|
|
475
|
+
|
|
476
|
+
@active_record_models = {}
|
|
477
|
+
|
|
478
|
+
tracker.models.each do |name, model|
|
|
479
|
+
if model.ancestor? :"ActiveRecord::Base"
|
|
480
|
+
@active_record_models[name] = model
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
@active_record_models
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def friendly_type_of input_type
|
|
488
|
+
if input_type.is_a? Match
|
|
489
|
+
input_type = input_type.type
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
case input_type
|
|
493
|
+
when :params
|
|
494
|
+
"parameter value"
|
|
495
|
+
when :cookies
|
|
496
|
+
"cookie value"
|
|
497
|
+
when :request
|
|
498
|
+
"request value"
|
|
499
|
+
when :model
|
|
500
|
+
"model attribute"
|
|
501
|
+
else
|
|
502
|
+
"user input"
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require 'railroader/checks/base_check'
|
|
2
|
+
|
|
3
|
+
#Checks if password is stored in controller
|
|
4
|
+
#when using http_basic_authenticate_with
|
|
5
|
+
#
|
|
6
|
+
#Only for Rails >= 3.1
|
|
7
|
+
class Railroader::CheckBasicAuth < Railroader::BaseCheck
|
|
8
|
+
Railroader::Checks.add self
|
|
9
|
+
|
|
10
|
+
@description = "Checks for the use of http_basic_authenticate_with"
|
|
11
|
+
|
|
12
|
+
def run_check
|
|
13
|
+
return if version_between? "0.0.0", "3.0.99"
|
|
14
|
+
|
|
15
|
+
check_basic_auth_filter
|
|
16
|
+
check_basic_auth_request
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def check_basic_auth_filter
|
|
20
|
+
controllers = tracker.controllers.select do |_name, c|
|
|
21
|
+
c.options[:http_basic_authenticate_with]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Hash[controllers].each do |name, controller|
|
|
25
|
+
controller.options[:http_basic_authenticate_with].each do |call|
|
|
26
|
+
|
|
27
|
+
if pass = get_password(call) and string? pass
|
|
28
|
+
warn :controller => name,
|
|
29
|
+
:warning_type => "Basic Auth",
|
|
30
|
+
:warning_code => :basic_auth_password,
|
|
31
|
+
:message => "Basic authentication password stored in source code",
|
|
32
|
+
:code => call,
|
|
33
|
+
:confidence => :high,
|
|
34
|
+
:file => controller.file
|
|
35
|
+
break
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Look for
|
|
42
|
+
# authenticate_or_request_with_http_basic do |username, password|
|
|
43
|
+
# username == "foo" && password == "bar"
|
|
44
|
+
# end
|
|
45
|
+
def check_basic_auth_request
|
|
46
|
+
tracker.find_call(:target => nil, :method => :authenticate_or_request_with_http_basic).each do |result|
|
|
47
|
+
if include_password_literal? result
|
|
48
|
+
warn :result => result,
|
|
49
|
+
:code => @include_password,
|
|
50
|
+
:warning_type => "Basic Auth",
|
|
51
|
+
:warning_code => :basic_auth_password,
|
|
52
|
+
:message => "Basic authentication password stored in source code",
|
|
53
|
+
:confidence => :high
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if the block of a result contains a comparison of password to string
|
|
59
|
+
def include_password_literal? result
|
|
60
|
+
@password_var = result[:block_args].last
|
|
61
|
+
@include_password = false
|
|
62
|
+
process result[:block]
|
|
63
|
+
@include_password
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Looks for :== calls on password var
|
|
67
|
+
def process_call exp
|
|
68
|
+
target = exp.target
|
|
69
|
+
|
|
70
|
+
if node_type?(target, :lvar) and
|
|
71
|
+
target.value == @password_var and
|
|
72
|
+
exp.method == :== and
|
|
73
|
+
string? exp.first_arg
|
|
74
|
+
|
|
75
|
+
@include_password = exp
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
exp
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def get_password call
|
|
82
|
+
arg = call.first_arg
|
|
83
|
+
|
|
84
|
+
return false if arg.nil? or not hash? arg
|
|
85
|
+
|
|
86
|
+
hash_access(arg, :password)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'railroader/checks/base_check'
|
|
2
|
+
|
|
3
|
+
class Railroader::CheckBasicAuthTimingAttack < Railroader::BaseCheck
|
|
4
|
+
Railroader::Checks.add self
|
|
5
|
+
|
|
6
|
+
@description = "Check for timing attack in basic auth (CVE-2015-7576)"
|
|
7
|
+
|
|
8
|
+
def run_check
|
|
9
|
+
@upgrade = case
|
|
10
|
+
when version_between?("0.0.0", "3.2.22")
|
|
11
|
+
"3.2.22.1"
|
|
12
|
+
when version_between?("4.0.0", "4.1.14")
|
|
13
|
+
"4.1.14.1"
|
|
14
|
+
when version_between?("4.2.0", "4.2.5")
|
|
15
|
+
"4.2.5.1"
|
|
16
|
+
else
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
check_basic_auth_call
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def check_basic_auth_call
|
|
24
|
+
tracker.find_call(target: nil, method: :http_basic_authenticate_with).each do |result|
|
|
25
|
+
warn :result => result,
|
|
26
|
+
:warning_type => "Timing Attack",
|
|
27
|
+
:warning_code => :CVE_2015_7576,
|
|
28
|
+
:message => "Basic authentication in Rails #{rails_version} is vulnerable to timing attacks. Upgrade to #@upgrade",
|
|
29
|
+
:confidence => :high,
|
|
30
|
+
:link => "https://groups.google.com/d/msg/rubyonrails-security/ANv0HDHEC3k/mt7wNGxbFQAJ"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|