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