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.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES +872 -0
  3. data/FEATURES +16 -0
  4. data/README.md +169 -0
  5. data/WARNING_TYPES +95 -0
  6. data/bin/brakeman +89 -0
  7. data/lib/brakeman.rb +495 -0
  8. data/lib/brakeman/app_tree.rb +161 -0
  9. data/lib/brakeman/brakeman.rake +17 -0
  10. data/lib/brakeman/call_index.rb +219 -0
  11. data/lib/brakeman/checks.rb +191 -0
  12. data/lib/brakeman/checks/base_check.rb +518 -0
  13. data/lib/brakeman/checks/check_basic_auth.rb +88 -0
  14. data/lib/brakeman/checks/check_basic_auth_timing_attack.rb +33 -0
  15. data/lib/brakeman/checks/check_content_tag.rb +160 -0
  16. data/lib/brakeman/checks/check_create_with.rb +75 -0
  17. data/lib/brakeman/checks/check_cross_site_scripting.rb +385 -0
  18. data/lib/brakeman/checks/check_default_routes.rb +86 -0
  19. data/lib/brakeman/checks/check_deserialize.rb +57 -0
  20. data/lib/brakeman/checks/check_detailed_exceptions.rb +55 -0
  21. data/lib/brakeman/checks/check_digest_dos.rb +38 -0
  22. data/lib/brakeman/checks/check_dynamic_finders.rb +49 -0
  23. data/lib/brakeman/checks/check_escape_function.rb +21 -0
  24. data/lib/brakeman/checks/check_evaluation.rb +36 -0
  25. data/lib/brakeman/checks/check_execute.rb +167 -0
  26. data/lib/brakeman/checks/check_file_access.rb +63 -0
  27. data/lib/brakeman/checks/check_file_disclosure.rb +35 -0
  28. data/lib/brakeman/checks/check_filter_skipping.rb +31 -0
  29. data/lib/brakeman/checks/check_forgery_setting.rb +74 -0
  30. data/lib/brakeman/checks/check_header_dos.rb +31 -0
  31. data/lib/brakeman/checks/check_i18n_xss.rb +48 -0
  32. data/lib/brakeman/checks/check_jruby_xml.rb +38 -0
  33. data/lib/brakeman/checks/check_json_encoding.rb +47 -0
  34. data/lib/brakeman/checks/check_json_parsing.rb +107 -0
  35. data/lib/brakeman/checks/check_link_to.rb +132 -0
  36. data/lib/brakeman/checks/check_link_to_href.rb +115 -0
  37. data/lib/brakeman/checks/check_mail_to.rb +49 -0
  38. data/lib/brakeman/checks/check_mass_assignment.rb +198 -0
  39. data/lib/brakeman/checks/check_mime_type_dos.rb +39 -0
  40. data/lib/brakeman/checks/check_model_attr_accessible.rb +55 -0
  41. data/lib/brakeman/checks/check_model_attributes.rb +119 -0
  42. data/lib/brakeman/checks/check_model_serialize.rb +67 -0
  43. data/lib/brakeman/checks/check_nested_attributes.rb +38 -0
  44. data/lib/brakeman/checks/check_nested_attributes_bypass.rb +58 -0
  45. data/lib/brakeman/checks/check_number_to_currency.rb +74 -0
  46. data/lib/brakeman/checks/check_quote_table_name.rb +40 -0
  47. data/lib/brakeman/checks/check_redirect.rb +215 -0
  48. data/lib/brakeman/checks/check_regex_dos.rb +69 -0
  49. data/lib/brakeman/checks/check_render.rb +92 -0
  50. data/lib/brakeman/checks/check_render_dos.rb +37 -0
  51. data/lib/brakeman/checks/check_render_inline.rb +54 -0
  52. data/lib/brakeman/checks/check_response_splitting.rb +21 -0
  53. data/lib/brakeman/checks/check_route_dos.rb +42 -0
  54. data/lib/brakeman/checks/check_safe_buffer_manipulation.rb +31 -0
  55. data/lib/brakeman/checks/check_sanitize_methods.rb +79 -0
  56. data/lib/brakeman/checks/check_secrets.rb +40 -0
  57. data/lib/brakeman/checks/check_select_tag.rb +60 -0
  58. data/lib/brakeman/checks/check_select_vulnerability.rb +60 -0
  59. data/lib/brakeman/checks/check_send.rb +48 -0
  60. data/lib/brakeman/checks/check_send_file.rb +19 -0
  61. data/lib/brakeman/checks/check_session_manipulation.rb +36 -0
  62. data/lib/brakeman/checks/check_session_settings.rb +170 -0
  63. data/lib/brakeman/checks/check_simple_format.rb +59 -0
  64. data/lib/brakeman/checks/check_single_quotes.rb +101 -0
  65. data/lib/brakeman/checks/check_skip_before_filter.rb +60 -0
  66. data/lib/brakeman/checks/check_sql.rb +660 -0
  67. data/lib/brakeman/checks/check_sql_cves.rb +101 -0
  68. data/lib/brakeman/checks/check_ssl_verify.rb +49 -0
  69. data/lib/brakeman/checks/check_strip_tags.rb +89 -0
  70. data/lib/brakeman/checks/check_symbol_dos.rb +64 -0
  71. data/lib/brakeman/checks/check_symbol_dos_cve.rb +30 -0
  72. data/lib/brakeman/checks/check_translate_bug.rb +45 -0
  73. data/lib/brakeman/checks/check_unsafe_reflection.rb +51 -0
  74. data/lib/brakeman/checks/check_unscoped_find.rb +41 -0
  75. data/lib/brakeman/checks/check_validation_regex.rb +116 -0
  76. data/lib/brakeman/checks/check_weak_hash.rb +151 -0
  77. data/lib/brakeman/checks/check_without_protection.rb +80 -0
  78. data/lib/brakeman/checks/check_xml_dos.rb +51 -0
  79. data/lib/brakeman/checks/check_yaml_parsing.rb +121 -0
  80. data/lib/brakeman/differ.rb +66 -0
  81. data/lib/brakeman/file_parser.rb +50 -0
  82. data/lib/brakeman/format/style.css +133 -0
  83. data/lib/brakeman/options.rb +301 -0
  84. data/lib/brakeman/parsers/rails2_erubis.rb +6 -0
  85. data/lib/brakeman/parsers/rails2_xss_plugin_erubis.rb +48 -0
  86. data/lib/brakeman/parsers/rails3_erubis.rb +74 -0
  87. data/lib/brakeman/parsers/template_parser.rb +89 -0
  88. data/lib/brakeman/processor.rb +102 -0
  89. data/lib/brakeman/processors/alias_processor.rb +1013 -0
  90. data/lib/brakeman/processors/base_processor.rb +277 -0
  91. data/lib/brakeman/processors/config_processor.rb +14 -0
  92. data/lib/brakeman/processors/controller_alias_processor.rb +273 -0
  93. data/lib/brakeman/processors/controller_processor.rb +326 -0
  94. data/lib/brakeman/processors/erb_template_processor.rb +80 -0
  95. data/lib/brakeman/processors/erubis_template_processor.rb +104 -0
  96. data/lib/brakeman/processors/gem_processor.rb +57 -0
  97. data/lib/brakeman/processors/haml_template_processor.rb +190 -0
  98. data/lib/brakeman/processors/lib/basic_processor.rb +37 -0
  99. data/lib/brakeman/processors/lib/find_all_calls.rb +223 -0
  100. data/lib/brakeman/processors/lib/find_call.rb +183 -0
  101. data/lib/brakeman/processors/lib/find_return_value.rb +134 -0
  102. data/lib/brakeman/processors/lib/processor_helper.rb +75 -0
  103. data/lib/brakeman/processors/lib/rails2_config_processor.rb +145 -0
  104. data/lib/brakeman/processors/lib/rails2_route_processor.rb +313 -0
  105. data/lib/brakeman/processors/lib/rails3_config_processor.rb +132 -0
  106. data/lib/brakeman/processors/lib/rails3_route_processor.rb +308 -0
  107. data/lib/brakeman/processors/lib/render_helper.rb +181 -0
  108. data/lib/brakeman/processors/lib/render_path.rb +107 -0
  109. data/lib/brakeman/processors/lib/route_helper.rb +68 -0
  110. data/lib/brakeman/processors/lib/safe_call_helper.rb +16 -0
  111. data/lib/brakeman/processors/library_processor.rb +119 -0
  112. data/lib/brakeman/processors/model_processor.rb +191 -0
  113. data/lib/brakeman/processors/output_processor.rb +171 -0
  114. data/lib/brakeman/processors/route_processor.rb +17 -0
  115. data/lib/brakeman/processors/slim_template_processor.rb +107 -0
  116. data/lib/brakeman/processors/template_alias_processor.rb +116 -0
  117. data/lib/brakeman/processors/template_processor.rb +74 -0
  118. data/lib/brakeman/report.rb +78 -0
  119. data/lib/brakeman/report/config/remediation.yml +71 -0
  120. data/lib/brakeman/report/ignore/config.rb +135 -0
  121. data/lib/brakeman/report/ignore/interactive.rb +311 -0
  122. data/lib/brakeman/report/renderer.rb +24 -0
  123. data/lib/brakeman/report/report_base.rb +286 -0
  124. data/lib/brakeman/report/report_codeclimate.rb +70 -0
  125. data/lib/brakeman/report/report_csv.rb +55 -0
  126. data/lib/brakeman/report/report_hash.rb +23 -0
  127. data/lib/brakeman/report/report_html.rb +216 -0
  128. data/lib/brakeman/report/report_json.rb +42 -0
  129. data/lib/brakeman/report/report_markdown.rb +156 -0
  130. data/lib/brakeman/report/report_table.rb +107 -0
  131. data/lib/brakeman/report/report_tabs.rb +17 -0
  132. data/lib/brakeman/report/templates/controller_overview.html.erb +22 -0
  133. data/lib/brakeman/report/templates/controller_warnings.html.erb +21 -0
  134. data/lib/brakeman/report/templates/error_overview.html.erb +29 -0
  135. data/lib/brakeman/report/templates/header.html.erb +58 -0
  136. data/lib/brakeman/report/templates/ignored_warnings.html.erb +25 -0
  137. data/lib/brakeman/report/templates/model_warnings.html.erb +21 -0
  138. data/lib/brakeman/report/templates/overview.html.erb +38 -0
  139. data/lib/brakeman/report/templates/security_warnings.html.erb +23 -0
  140. data/lib/brakeman/report/templates/template_overview.html.erb +21 -0
  141. data/lib/brakeman/report/templates/view_warnings.html.erb +34 -0
  142. data/lib/brakeman/report/templates/warning_overview.html.erb +17 -0
  143. data/lib/brakeman/rescanner.rb +483 -0
  144. data/lib/brakeman/scanner.rb +317 -0
  145. data/lib/brakeman/tracker.rb +347 -0
  146. data/lib/brakeman/tracker/collection.rb +93 -0
  147. data/lib/brakeman/tracker/config.rb +101 -0
  148. data/lib/brakeman/tracker/constants.rb +101 -0
  149. data/lib/brakeman/tracker/controller.rb +161 -0
  150. data/lib/brakeman/tracker/library.rb +17 -0
  151. data/lib/brakeman/tracker/model.rb +90 -0
  152. data/lib/brakeman/tracker/template.rb +33 -0
  153. data/lib/brakeman/util.rb +481 -0
  154. data/lib/brakeman/version.rb +3 -0
  155. data/lib/brakeman/warning.rb +255 -0
  156. data/lib/brakeman/warning_codes.rb +111 -0
  157. data/lib/ruby_parser/bm_sexp.rb +610 -0
  158. data/lib/ruby_parser/bm_sexp_processor.rb +116 -0
  159. 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