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,200 @@
|
|
|
1
|
+
require 'railroader/checks/check_cross_site_scripting'
|
|
2
|
+
|
|
3
|
+
#Checks for unescaped values in `content_tag`
|
|
4
|
+
#
|
|
5
|
+
# content_tag :tag, body
|
|
6
|
+
# ^-- Unescaped in Rails 2.x
|
|
7
|
+
#
|
|
8
|
+
# content_tag, :tag, body, attribute => value
|
|
9
|
+
# ^-- Unescaped in all versions
|
|
10
|
+
#
|
|
11
|
+
# content_tag, :tag, body, attribute => value
|
|
12
|
+
# ^
|
|
13
|
+
# |
|
|
14
|
+
# Escaped by default, can be explicitly escaped
|
|
15
|
+
# or not by passing in (true|false) as fourth argument
|
|
16
|
+
class Railroader::CheckContentTag < Railroader::CheckCrossSiteScripting
|
|
17
|
+
Railroader::Checks.add self
|
|
18
|
+
|
|
19
|
+
@description = "Checks for XSS in calls to content_tag"
|
|
20
|
+
|
|
21
|
+
def run_check
|
|
22
|
+
@ignore_methods = Set[:button_to, :check_box, :escapeHTML, :escape_once,
|
|
23
|
+
:field_field, :fields_for, :h, :hidden_field,
|
|
24
|
+
:hidden_field, :hidden_field_tag, :image_tag, :label,
|
|
25
|
+
:mail_to, :radio_button, :select,
|
|
26
|
+
:submit_tag, :text_area, :text_field,
|
|
27
|
+
:text_field_tag, :url_encode, :u, :url_for,
|
|
28
|
+
:will_paginate].merge tracker.options[:safe_methods]
|
|
29
|
+
|
|
30
|
+
@known_dangerous = []
|
|
31
|
+
@content_tags = tracker.find_call :target => false, :method => :content_tag
|
|
32
|
+
|
|
33
|
+
@models = tracker.models.keys
|
|
34
|
+
@inspect_arguments = tracker.options[:check_arguments]
|
|
35
|
+
@mark = nil
|
|
36
|
+
|
|
37
|
+
Railroader.debug "Checking for XSS in content_tag"
|
|
38
|
+
@content_tags.each do |call|
|
|
39
|
+
process_result call
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
check_cve_2016_6316
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def process_result result
|
|
46
|
+
return if duplicate? result
|
|
47
|
+
|
|
48
|
+
call = result[:call] = result[:call].dup
|
|
49
|
+
|
|
50
|
+
args = call.arglist
|
|
51
|
+
|
|
52
|
+
tag_name = args[1]
|
|
53
|
+
content = args[2]
|
|
54
|
+
attributes = args[3]
|
|
55
|
+
escape_attr = args[4]
|
|
56
|
+
|
|
57
|
+
@matched = false
|
|
58
|
+
|
|
59
|
+
#Silly, but still dangerous if someone uses user input in the tag type
|
|
60
|
+
check_argument result, tag_name
|
|
61
|
+
|
|
62
|
+
#Versions before 3.x do not escape body of tag, nor does the rails_xss gem
|
|
63
|
+
unless @matched or (tracker.options[:rails3] and not raw? content)
|
|
64
|
+
check_argument result, content
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#Attribute keys are never escaped, so check them for user input
|
|
68
|
+
if not @matched and hash? attributes and not request_value? attributes
|
|
69
|
+
hash_iterate(attributes) do |k, _v|
|
|
70
|
+
check_argument result, k
|
|
71
|
+
return if @matched
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#By default, content_tag escapes attribute values passed in as a hash.
|
|
76
|
+
#But this behavior can be disabled. So only check attributes hash
|
|
77
|
+
#if they are explicitly not escaped.
|
|
78
|
+
if not @matched and attributes and (false? escape_attr or cve_2016_6316?)
|
|
79
|
+
if request_value? attributes or not hash? attributes
|
|
80
|
+
check_argument result, attributes
|
|
81
|
+
else #check hash values
|
|
82
|
+
hash_iterate(attributes) do |_k, v|
|
|
83
|
+
check_argument result, v
|
|
84
|
+
return if @matched
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check_argument result, exp
|
|
91
|
+
#Check contents of raw() calls directly
|
|
92
|
+
if raw? exp
|
|
93
|
+
arg = process exp.first_arg
|
|
94
|
+
else
|
|
95
|
+
arg = process exp
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if input = has_immediate_user_input?(arg)
|
|
99
|
+
message = "Unescaped #{friendly_type_of input} in content_tag"
|
|
100
|
+
|
|
101
|
+
add_result result
|
|
102
|
+
|
|
103
|
+
warn :result => result,
|
|
104
|
+
:warning_type => "Cross-Site Scripting",
|
|
105
|
+
:warning_code => :xss_content_tag,
|
|
106
|
+
:message => message,
|
|
107
|
+
:user_input => input,
|
|
108
|
+
:confidence => :high,
|
|
109
|
+
:link_path => "content_tag"
|
|
110
|
+
|
|
111
|
+
elsif not tracker.options[:ignore_model_output] and match = has_immediate_model?(arg)
|
|
112
|
+
unless IGNORE_MODEL_METHODS.include? match.method
|
|
113
|
+
add_result result
|
|
114
|
+
|
|
115
|
+
if likely_model_attribute? match
|
|
116
|
+
confidence = :high
|
|
117
|
+
else
|
|
118
|
+
confidence = :medium
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
warn :result => result,
|
|
122
|
+
:warning_type => "Cross-Site Scripting",
|
|
123
|
+
:warning_code => :xss_content_tag,
|
|
124
|
+
:message => "Unescaped model attribute in content_tag",
|
|
125
|
+
:user_input => match,
|
|
126
|
+
:confidence => confidence,
|
|
127
|
+
:link_path => "content_tag"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
elsif @matched
|
|
131
|
+
return if @matched.type == :model and tracker.options[:ignore_model_output]
|
|
132
|
+
|
|
133
|
+
message = "Unescaped #{friendly_type_of @matched} in content_tag"
|
|
134
|
+
|
|
135
|
+
add_result result
|
|
136
|
+
|
|
137
|
+
warn :result => result,
|
|
138
|
+
:warning_type => "Cross-Site Scripting",
|
|
139
|
+
:warning_code => :xss_content_tag,
|
|
140
|
+
:message => message,
|
|
141
|
+
:user_input => @matched,
|
|
142
|
+
:confidence => :medium,
|
|
143
|
+
:link_path => "content_tag"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def process_call exp
|
|
148
|
+
if @mark
|
|
149
|
+
actually_process_call exp
|
|
150
|
+
else
|
|
151
|
+
@mark = true
|
|
152
|
+
actually_process_call exp
|
|
153
|
+
@mark = false
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
exp
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def check_cve_2016_6316
|
|
160
|
+
if cve_2016_6316?
|
|
161
|
+
confidence = if @content_tags.any?
|
|
162
|
+
:high
|
|
163
|
+
else
|
|
164
|
+
:medium
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
fix_version = case
|
|
168
|
+
when version_between?("3.0.0", "3.2.22.3")
|
|
169
|
+
"3.2.22.4"
|
|
170
|
+
when version_between?("4.0.0", "4.2.7.0")
|
|
171
|
+
"4.2.7.1"
|
|
172
|
+
when version_between?("5.0.0", "5.0.0")
|
|
173
|
+
"5.0.0.1"
|
|
174
|
+
when (version.nil? and tracker.options[:rails3])
|
|
175
|
+
"3.2.22.4"
|
|
176
|
+
when (version.nil? and tracker.options[:rails4])
|
|
177
|
+
"4.2.7.2"
|
|
178
|
+
else
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
warn :warning_type => "Cross-Site Scripting",
|
|
183
|
+
:warning_code => :CVE_2016_6316,
|
|
184
|
+
:message => "Rails #{rails_version} content_tag does not escape double quotes in attribute values (CVE-2016-6316). Upgrade to #{fix_version}",
|
|
185
|
+
:confidence => confidence,
|
|
186
|
+
:gem_info => gemfile_or_environment,
|
|
187
|
+
:link_path => "https://groups.google.com/d/msg/ruby-security-ann/8B2iV2tPRSE/JkjCJkSoCgAJ"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def raw? exp
|
|
192
|
+
call? exp and exp.method == :raw
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def cve_2016_6316?
|
|
196
|
+
version_between? "3.0.0", "3.2.22.3" or
|
|
197
|
+
version_between? "4.0.0", "4.2.7.0" or
|
|
198
|
+
version_between? "5.0.0", "5.0.0.0"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'railroader/checks/base_check'
|
|
2
|
+
|
|
3
|
+
class Railroader::CheckCreateWith < Railroader::BaseCheck
|
|
4
|
+
Railroader::Checks.add self
|
|
5
|
+
|
|
6
|
+
@description = "Checks for strong params bypass in CVE-2014-3514"
|
|
7
|
+
|
|
8
|
+
def run_check
|
|
9
|
+
@warned = false
|
|
10
|
+
|
|
11
|
+
if version_between? "4.0.0", "4.0.8"
|
|
12
|
+
suggested_version = "4.0.9"
|
|
13
|
+
elsif version_between? "4.1.0", "4.1.4"
|
|
14
|
+
suggested_version = "4.1.5"
|
|
15
|
+
else
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@message = "create_with is vulnerable to strong params bypass. Upgrade to Rails #{suggested_version} or patch"
|
|
20
|
+
|
|
21
|
+
tracker.find_call(:method => :create_with, :nested => true).each do |result|
|
|
22
|
+
process_result result
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
generic_warning unless @warned
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def process_result result
|
|
29
|
+
return unless original? result
|
|
30
|
+
arg = result[:call].first_arg
|
|
31
|
+
|
|
32
|
+
confidence = danger_level arg
|
|
33
|
+
|
|
34
|
+
if confidence
|
|
35
|
+
@warned = true
|
|
36
|
+
|
|
37
|
+
warn :warning_type => "Mass Assignment",
|
|
38
|
+
:warning_code => :CVE_2014_3514_call,
|
|
39
|
+
:result => result,
|
|
40
|
+
:message => @message,
|
|
41
|
+
:confidence => confidence,
|
|
42
|
+
:link_path => "https://groups.google.com/d/msg/rubyonrails-security/M4chq5Sb540/CC1Fh0Y_NWwJ"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#For a given create_with call, set confidence level.
|
|
47
|
+
#Ignore calls that use permit()
|
|
48
|
+
def danger_level exp
|
|
49
|
+
return unless sexp? exp
|
|
50
|
+
|
|
51
|
+
if call? exp and exp.method == :permit
|
|
52
|
+
nil
|
|
53
|
+
elsif request_value? exp
|
|
54
|
+
:high
|
|
55
|
+
elsif hash? exp
|
|
56
|
+
nil
|
|
57
|
+
elsif has_immediate_user_input?(exp)
|
|
58
|
+
:high
|
|
59
|
+
elsif include_user_input? exp
|
|
60
|
+
:medium
|
|
61
|
+
else
|
|
62
|
+
:weak
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def generic_warning
|
|
67
|
+
warn :warning_type => "Mass Assignment",
|
|
68
|
+
:warning_code => :CVE_2014_3514,
|
|
69
|
+
:message => @message,
|
|
70
|
+
:gem_info => gemfile_or_environment,
|
|
71
|
+
:confidence => :medium,
|
|
72
|
+
:link_path => "https://groups.google.com/d/msg/rubyonrails-security/M4chq5Sb540/CC1Fh0Y_NWwJ"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
require 'railroader/checks/base_check'
|
|
2
|
+
require 'railroader/processors/lib/find_call'
|
|
3
|
+
require 'railroader/processors/lib/processor_helper'
|
|
4
|
+
require 'railroader/util'
|
|
5
|
+
require 'set'
|
|
6
|
+
|
|
7
|
+
#This check looks for unescaped output in templates which contains
|
|
8
|
+
#parameters or model attributes.
|
|
9
|
+
#
|
|
10
|
+
#For example:
|
|
11
|
+
#
|
|
12
|
+
# <%= User.find(:id).name %>
|
|
13
|
+
# <%= params[:id] %>
|
|
14
|
+
class Railroader::CheckCrossSiteScripting < Railroader::BaseCheck
|
|
15
|
+
Railroader::Checks.add self
|
|
16
|
+
|
|
17
|
+
@description = "Checks for unescaped output in views"
|
|
18
|
+
|
|
19
|
+
#Model methods which are known to be harmless
|
|
20
|
+
IGNORE_MODEL_METHODS = Set[:average, :count, :maximum, :minimum, :sum, :id]
|
|
21
|
+
|
|
22
|
+
MODEL_METHODS = Set[:all, :find, :first, :last, :new]
|
|
23
|
+
|
|
24
|
+
IGNORE_LIKE = /^link_to_|(_path|_tag|_url)$/
|
|
25
|
+
|
|
26
|
+
HAML_HELPERS = Sexp.new(:colon2, Sexp.new(:const, :Haml), :Helpers)
|
|
27
|
+
|
|
28
|
+
XML_HELPER = Sexp.new(:colon2, Sexp.new(:const, :Erubis), :XmlHelper)
|
|
29
|
+
|
|
30
|
+
URI = Sexp.new(:const, :URI)
|
|
31
|
+
|
|
32
|
+
CGI = Sexp.new(:const, :CGI)
|
|
33
|
+
|
|
34
|
+
FORM_BUILDER = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new)
|
|
35
|
+
|
|
36
|
+
#Run check
|
|
37
|
+
def run_check
|
|
38
|
+
setup
|
|
39
|
+
|
|
40
|
+
tracker.each_template do |name, template|
|
|
41
|
+
Railroader.debug "Checking #{name} for XSS"
|
|
42
|
+
|
|
43
|
+
@current_template = template
|
|
44
|
+
|
|
45
|
+
template.each_output do |out|
|
|
46
|
+
unless check_for_immediate_xss out
|
|
47
|
+
@matched = false
|
|
48
|
+
@mark = false
|
|
49
|
+
process out
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_for_immediate_xss exp
|
|
56
|
+
return :duplicate if duplicate? exp
|
|
57
|
+
|
|
58
|
+
if exp.node_type == :output
|
|
59
|
+
out = exp.value
|
|
60
|
+
elsif exp.node_type == :escaped_output
|
|
61
|
+
if raw_call? exp
|
|
62
|
+
out = exp.value.first_arg
|
|
63
|
+
elsif html_safe_call? exp
|
|
64
|
+
out = exp.value.target
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
return if call? out and ignore_call? out.target, out.method
|
|
69
|
+
|
|
70
|
+
if input = has_immediate_user_input?(out)
|
|
71
|
+
add_result exp
|
|
72
|
+
|
|
73
|
+
message = "Unescaped #{friendly_type_of input}"
|
|
74
|
+
|
|
75
|
+
warn :template => @current_template,
|
|
76
|
+
:warning_type => "Cross-Site Scripting",
|
|
77
|
+
:warning_code => :cross_site_scripting,
|
|
78
|
+
:message => message,
|
|
79
|
+
:code => input.match,
|
|
80
|
+
:confidence => :high
|
|
81
|
+
|
|
82
|
+
elsif not tracker.options[:ignore_model_output] and match = has_immediate_model?(out)
|
|
83
|
+
method = if call? match
|
|
84
|
+
match.method
|
|
85
|
+
else
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless IGNORE_MODEL_METHODS.include? method
|
|
90
|
+
add_result exp
|
|
91
|
+
|
|
92
|
+
if likely_model_attribute? match
|
|
93
|
+
confidence = :high
|
|
94
|
+
else
|
|
95
|
+
confidence = :medium
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
message = "Unescaped model attribute"
|
|
99
|
+
link_path = "cross_site_scripting"
|
|
100
|
+
warning_code = :cross_site_scripting
|
|
101
|
+
|
|
102
|
+
if node_type?(out, :call, :safe_call, :attrasgn, :safe_attrasgn) && out.method == :to_json
|
|
103
|
+
message += " in JSON hash"
|
|
104
|
+
link_path += "_to_json"
|
|
105
|
+
warning_code = :xss_to_json
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
warn :template => @current_template,
|
|
109
|
+
:warning_type => "Cross-Site Scripting",
|
|
110
|
+
:warning_code => warning_code,
|
|
111
|
+
:message => message,
|
|
112
|
+
:code => match,
|
|
113
|
+
:confidence => confidence,
|
|
114
|
+
:link_path => link_path
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
else
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
#Call already involves a model, but might not be acting on a record
|
|
123
|
+
def likely_model_attribute? exp
|
|
124
|
+
return false unless call? exp
|
|
125
|
+
|
|
126
|
+
method = exp.method
|
|
127
|
+
|
|
128
|
+
if MODEL_METHODS.include? method or method.to_s.start_with? "find_by_"
|
|
129
|
+
true
|
|
130
|
+
else
|
|
131
|
+
likely_model_attribute? exp.target
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#Process an output Sexp
|
|
136
|
+
def process_output exp
|
|
137
|
+
process exp.value.dup
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#Look for calls to raw()
|
|
141
|
+
#Otherwise, ignore
|
|
142
|
+
def process_escaped_output exp
|
|
143
|
+
unless check_for_immediate_xss exp
|
|
144
|
+
if not duplicate? exp
|
|
145
|
+
if raw_call? exp
|
|
146
|
+
process exp.value.first_arg
|
|
147
|
+
elsif html_safe_call? exp
|
|
148
|
+
process exp.value.target
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
exp
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
#Check a call for user input
|
|
156
|
+
#
|
|
157
|
+
#
|
|
158
|
+
#Since we want to report an entire call and not just part of one, use @mark
|
|
159
|
+
#to mark when a call is started. Any dangerous values inside will then
|
|
160
|
+
#report the entire call chain.
|
|
161
|
+
def process_call exp
|
|
162
|
+
if @mark
|
|
163
|
+
actually_process_call exp
|
|
164
|
+
else
|
|
165
|
+
@mark = true
|
|
166
|
+
actually_process_call exp
|
|
167
|
+
message = nil
|
|
168
|
+
|
|
169
|
+
if @matched
|
|
170
|
+
unless @matched.type and tracker.options[:ignore_model_output]
|
|
171
|
+
message = "Unescaped #{friendly_type_of @matched}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if message and not duplicate? exp
|
|
175
|
+
add_result exp
|
|
176
|
+
|
|
177
|
+
link_path = "cross_site_scripting"
|
|
178
|
+
warning_code = :cross_site_scripting
|
|
179
|
+
|
|
180
|
+
if @known_dangerous.include? exp.method
|
|
181
|
+
confidence = :high
|
|
182
|
+
if exp.method == :to_json
|
|
183
|
+
message += " in JSON hash"
|
|
184
|
+
link_path += "_to_json"
|
|
185
|
+
warning_code = :xss_to_json
|
|
186
|
+
end
|
|
187
|
+
else
|
|
188
|
+
confidence = :weak
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
warn :template => @current_template,
|
|
192
|
+
:warning_type => "Cross-Site Scripting",
|
|
193
|
+
:warning_code => warning_code,
|
|
194
|
+
:message => message,
|
|
195
|
+
:code => exp,
|
|
196
|
+
:user_input => @matched,
|
|
197
|
+
:confidence => confidence,
|
|
198
|
+
:link_path => link_path
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
@mark = @matched = false
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
exp
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def actually_process_call exp
|
|
209
|
+
return if @matched
|
|
210
|
+
target = exp.target
|
|
211
|
+
if sexp? target
|
|
212
|
+
target = process target
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
method = exp.method
|
|
216
|
+
|
|
217
|
+
#Ignore safe items
|
|
218
|
+
if ignore_call? target, method
|
|
219
|
+
@matched = false
|
|
220
|
+
elsif sexp? target and model_name? target[1] #TODO: use method call?
|
|
221
|
+
@matched = Match.new(:model, exp)
|
|
222
|
+
elsif cookies? exp
|
|
223
|
+
@matched = Match.new(:cookies, exp)
|
|
224
|
+
elsif @inspect_arguments and params? exp
|
|
225
|
+
@matched = Match.new(:params, exp)
|
|
226
|
+
elsif @inspect_arguments
|
|
227
|
+
process_call_args exp
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
#Note that params have been found
|
|
232
|
+
def process_params exp
|
|
233
|
+
@matched = Match.new(:params, exp)
|
|
234
|
+
exp
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
#Note that cookies have been found
|
|
238
|
+
def process_cookies exp
|
|
239
|
+
@matched = Match.new(:cookies, exp)
|
|
240
|
+
exp
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
#Ignore calls to render
|
|
244
|
+
def process_render exp
|
|
245
|
+
exp
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
#Process as default
|
|
249
|
+
def process_dstr exp
|
|
250
|
+
process_default exp
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
#Process as default
|
|
254
|
+
def process_format exp
|
|
255
|
+
process_default exp
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
#Ignore output HTML escaped via HAML
|
|
259
|
+
def process_format_escaped exp
|
|
260
|
+
exp
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
#Ignore condition in if Sexp
|
|
264
|
+
def process_if exp
|
|
265
|
+
process exp.then_clause if sexp? exp.then_clause
|
|
266
|
+
process exp.else_clause if sexp? exp.else_clause
|
|
267
|
+
exp
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def process_case exp
|
|
271
|
+
#Ignore user input in case value
|
|
272
|
+
#TODO: also ignore when values
|
|
273
|
+
|
|
274
|
+
current = 2
|
|
275
|
+
while current < exp.length
|
|
276
|
+
process exp[current] if exp[current]
|
|
277
|
+
current += 1
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
exp
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def setup
|
|
284
|
+
@ignore_methods = Set[:==, :!=, :button_to, :check_box, :content_tag, :escapeHTML, :escape_once,
|
|
285
|
+
:field_field, :fields_for, :h, :hidden_field,
|
|
286
|
+
:hidden_field, :hidden_field_tag, :image_tag, :label,
|
|
287
|
+
:link_to, :mail_to, :radio_button, :select,
|
|
288
|
+
:submit_tag, :text_area, :text_field,
|
|
289
|
+
:text_field_tag, :url_encode, :u, :url_for,
|
|
290
|
+
:will_paginate].merge tracker.options[:safe_methods]
|
|
291
|
+
|
|
292
|
+
@models = tracker.models.keys
|
|
293
|
+
@inspect_arguments = tracker.options[:check_arguments]
|
|
294
|
+
|
|
295
|
+
@known_dangerous = Set[:truncate, :concat]
|
|
296
|
+
|
|
297
|
+
if version_between? "2.0.0", "3.0.5"
|
|
298
|
+
@known_dangerous << :auto_link
|
|
299
|
+
elsif version_between? "3.0.6", "3.0.99"
|
|
300
|
+
@ignore_methods << :auto_link
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if version_between? "2.0.0", "2.3.14" or tracker.config.gem_version(:'rails-html-sanitizer') == '1.0.2'
|
|
304
|
+
@known_dangerous << :strip_tags
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if tracker.config.has_gem? :'rails-html-sanitizer' and
|
|
308
|
+
version_between? "1.0.0", "1.0.2", tracker.config.gem_version(:'rails-html-sanitizer')
|
|
309
|
+
|
|
310
|
+
@known_dangerous << :sanitize
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
json_escape_on = false
|
|
314
|
+
initializers = tracker.check_initializers :ActiveSupport, :escape_html_entities_in_json=
|
|
315
|
+
initializers.each {|result| json_escape_on = true?(result.call.first_arg) }
|
|
316
|
+
|
|
317
|
+
if tracker.config.escape_html_entities_in_json?
|
|
318
|
+
json_escape_on = true
|
|
319
|
+
elsif version_between? "4.0.0", "9.9.9"
|
|
320
|
+
json_escape_on = true
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
if !json_escape_on or version_between? "0.0.0", "2.0.99"
|
|
324
|
+
@known_dangerous << :to_json
|
|
325
|
+
Railroader.debug("Automatic to_json escaping not enabled, consider to_json dangerous")
|
|
326
|
+
else
|
|
327
|
+
@safe_input_attributes << :to_json
|
|
328
|
+
Railroader.debug("Automatic to_json escaping is enabled.")
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def raw_call? exp
|
|
333
|
+
exp.value.node_type == :call and exp.value.method == :raw
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def html_safe_call? exp
|
|
337
|
+
call? exp.value and exp.value.method == :html_safe
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def ignore_call? target, method
|
|
341
|
+
ignored_method?(target, method) or
|
|
342
|
+
safe_input_attribute?(target, method) or
|
|
343
|
+
ignored_model_method?(target, method) or
|
|
344
|
+
form_builder_method?(target, method) or
|
|
345
|
+
haml_escaped?(target, method) or
|
|
346
|
+
boolean_method?(method) or
|
|
347
|
+
cgi_escaped?(target, method) or
|
|
348
|
+
xml_escaped?(target, method)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def ignored_model_method? target, method
|
|
352
|
+
((@matched and @matched.type == :model) or
|
|
353
|
+
model_name? target) and
|
|
354
|
+
IGNORE_MODEL_METHODS.include? method
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def ignored_method? target, method
|
|
358
|
+
@ignore_methods.include? method or method.to_s =~ IGNORE_LIKE
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def cgi_escaped? target, method
|
|
362
|
+
method == :escape and
|
|
363
|
+
(target == URI or target == CGI)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def haml_escaped? target, method
|
|
367
|
+
method == :html_escape and target == HAML_HELPERS
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def xml_escaped? target, method
|
|
371
|
+
method == :escape_xml and target == XML_HELPER
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def form_builder_method? target, method
|
|
375
|
+
target == FORM_BUILDER and @ignore_methods.include? method
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def safe_input_attribute? target, method
|
|
379
|
+
target and always_safe_method? method
|
|
380
|
+
end
|
|
381
|
+
end
|