rubocop-betterment 1.13.0 → 1.19.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc4eabc0685d064150b4545175e96ee1c848ad32f53bf35f83b5125a7c309b82
4
- data.tar.gz: 258718477b39bb37af975daae5f1d4db1ea7e4adb55d7b8ee6b3757aee5dbfbd
3
+ metadata.gz: e45880e05081ae8b62a4b1e1fb9778a5580b9b17fe839973f1046242222881e1
4
+ data.tar.gz: 4212f8e0f60a70d61db53a2f9af447248efc686d593af0145c715a5a831db683
5
5
  SHA512:
6
- metadata.gz: 692eb066b0e6b45d0c7049f90acc66c600d1a6b24793ba691975112ccd5a7ddcfc01b3674dc180fecf9eb75dd4445c826d30864b4fbea1768e53df1fceb1c2e4
7
- data.tar.gz: 7cee25dc098dbabbdac9ae42a10ada567047e211c445cb6ecef7405ae6da89a03927c044f3fb9781a2bb6e48b7fb3fa1f608f820076d4828159477cee8864914
6
+ metadata.gz: be0a7163ee430bcc93333c227cdbe56180864dbac0f128e0e70fc526a2a82b202d36f8e54fad30d8bdf33c66fc668c9ad207ddc29d1f6eaeea97b35f4a676070
7
+ data.tar.gz: a4aefcc523eb8c4c63ce31d851e4a47461288a6ee152e9a53572258135778098e55e7b470d1058f2dbd283d4d9c45abcd714717327db5ee1d6114d4f0bfd8d91
data/README.md CHANGED
@@ -20,13 +20,104 @@ inherit_gem:
20
20
  - config/default.yml
21
21
  ```
22
22
 
23
- ## Custom Cops
24
-
25
- All cops are located under [`lib/rubocop/cop/betterment`](lib/rubocop/cop/betterment)
26
-
27
23
  ## Dependencies
28
24
 
29
25
  This gem depends on the following other gems:
30
26
 
31
27
  - rubocop
32
28
  - rubocop-rspec
29
+
30
+ ## Custom Cops
31
+
32
+ All cops are located under [`lib/rubocop/cop/betterment`](lib/rubocop/cop/betterment)
33
+
34
+ ### Betterment/AuthorizationInController
35
+
36
+ This cop looks for unsafe handling of id-like parameters in controllers that may lead to mass assignment style vulnerabilities. It does this by tracking methods that retrieve input from the client and variables that hold onto these values. Any models initialized or updated using these values will then be flagged by the cop. Take this example controller:
37
+
38
+ ```ruby
39
+ class Controller
40
+ def create_params
41
+ params.permit(:user_id, :language)
42
+ end
43
+
44
+ def create
45
+ info = params.permit(:user_id)
46
+ Model.new(user_id: info[:user_id], language: params[:language])
47
+ Model.new(user_id: params[:user_id], language: params[:language])
48
+ Model.new(create_params)
49
+ end
50
+ end
51
+ ```
52
+
53
+ All three `Model.new` calls may be susceptible to a mass assignment vulnerability. To address these vulnerabilities, some form of authorization will be needed to ensure that the user issuing this request is allowed to create a `Model` that references the specific `user_id`. To get a better understanding of what this cop flags and doesn't flag, take a look at its [spec](spec/rubocop/cop/betterment/authorization_in_controller_spec.rb).
54
+
55
+ In cases where more fine-grained control over what parameters are considered sensitive is desired, two configuration options can be used: `unsafe_parameters` and `unsafe_regex`. By default this cop will flag unsafe uses of any parameters whose names end in `_id`, but additional parameters can be specified by configuring `unsafe_parameters`. In cases where the default pattern of `.*_id` is insufficient or incorrect, this regex can be swapped out by specifying the `unsafe_regex` configuration option. In total, this cop will flag any parameters whose names are on the `unsafe_parameters` list or matches the `unsafe_regex` pattern.
56
+
57
+ This is what a full configuration of this cop may look like:
58
+
59
+ ```yaml
60
+ Betterment/AuthorizationInController:
61
+ # Limit this cop just to controllers
62
+ Include:
63
+ - 'app/controllers/**/*.rb'
64
+ unsafe_parameters:
65
+ - username
66
+ - misc_unsafe_parameter
67
+ unsafe_regex: '.*_id$'
68
+ ```
69
+
70
+ ### Betterment/UnscopedFind
71
+
72
+ This cop flags code that passes user input directly into a `find`-like call that may lead to authorization issues. For example, a controller that uses user input to find a document will need to ensure that the user is authorized to access that document. Take the following sample:
73
+
74
+ ```ruby
75
+ class Controller
76
+ def index
77
+ @document = Document.find(params[:document_id])
78
+ end
79
+ end
80
+ ```
81
+
82
+ In this case, `@document` may not belong to the user and authorization will have to be done somewhere else, potentially introducing a vulnerability. One way to address this violation is to replace the `Document.find(...)` call with a `current_user.documents.find(...)` call. This fails fast when `current_user` is not authorized to access the document, without an extra authorization check that a `Document.find` call would require.
83
+
84
+ When dealing with models whose data is not ever considered private, it may make sense to add them to the `unauthenticated_models` configuration option. For example, reference data such as `ZipCode` or `Language` may be represented using models, but may not make sense to enforce any form of authentication. Take the sample controller below:
85
+
86
+ ```ruby
87
+ class Controller < UnauthenticatedWebappController
88
+ def index
89
+ @language = Language.find(params[:language])
90
+ @zip = ZipCode.find(params[:zip])
91
+ end
92
+ end
93
+ ```
94
+
95
+ There is nothing specific to a user or otherwise anything sensitive about `Language` or `ZipCode`. The cop can be configured to treat these models as unauthenticated so that calling `find`-like methods with them will not trigger any violations:
96
+
97
+ ```yaml
98
+ Betterment/UnscopedFind:
99
+ unauthenticated_models:
100
+ - Language
101
+ - ZipCode
102
+ ```
103
+
104
+ ### Betterment/DynamicParams
105
+
106
+ This cop flags code that accesses parameters whose names may be dynamically generated, such as a list of parameters in an a global variable or a return value from a method. In some cases, dynamically accessing parameter names can obscure what the client is expected to send and may make it difficult to reason about the code, both manually and programmatically. For example:
107
+
108
+ ```ruby
109
+ class Controller
110
+ def create_param_names
111
+ %i(user_id first_name last_name)
112
+ end
113
+
114
+ def create
115
+ parameter_name = :user_id
116
+ params.permit(parameter_name)
117
+ params.permit(create_params_names)
118
+ params.permit(%w(blog post comment).flat_map { |p| ["#{p}_name", "#{p}_title"] })
119
+ end
120
+ end
121
+ ```
122
+
123
+ All three `params.permit` calls will be flagged.
@@ -87,4 +87,25 @@ user1 = User.first
87
87
  user2 = User.second
88
88
  ```
89
89
 
90
- The snake case style is more readable.
90
+ The snake case style is more readable.
91
+
92
+ ## Betterment/ServerErrorAssertion
93
+
94
+ In RSpec tests, we prevent HTTP response status assertions against server error codes (e.g., 500). While it’s acceptable to
95
+ “under-build” APIs under assumption of controlled and well-behaving clients, these exceptions should be treated as undefined behavior and
96
+ thus do not need request spec coverage. In cases where the server must communicate an expected failure to the client, an appropriate
97
+ semantic status code must be used (e.g., 403, 422, etc.).
98
+
99
+ ### GOOD:
100
+
101
+ ```ruby
102
+ expect(response).to have_http_status :forbidden
103
+ expect(response).to have_http_status 422
104
+ ```
105
+
106
+ ### BAD:
107
+
108
+ ```ruby
109
+ expect(response).to have_http_status :internal_server_error
110
+ expect(response).to have_http_status 500
111
+ ```
@@ -17,6 +17,15 @@ AllCops:
17
17
  DisplayStyleGuide: true
18
18
  DisplayCopNames: true
19
19
 
20
+ Betterment/ServerErrorAssertion:
21
+ Description: 'Detects assertions on 5XX HTTP statuses.'
22
+ Include:
23
+ - 'spec/requests/**/*_spec.rb'
24
+
25
+ Betterment/AuthorizationInController:
26
+ Description: 'Detects unsafe handling of id-like parameters in controllers.'
27
+ Enabled: false
28
+
20
29
  Betterment/SitePrismLoaded:
21
30
  Include:
22
31
  - 'spec/features/**/*_spec.rb'
@@ -1,6 +1,13 @@
1
1
  require 'rubocop'
2
+ require 'rubocop/cop/betterment/utils/parser'
3
+ require 'rubocop/cop/betterment/authorization_in_controller'
4
+ require 'rubocop/cop/betterment/dynamic_params'
5
+ require 'rubocop/cop/betterment/unscoped_find'
2
6
  require 'rubocop/cop/betterment/timeout'
3
7
  require 'rubocop/cop/betterment/memoization_with_arguments'
4
8
  require 'rubocop/cop/betterment/site_prism_loaded'
5
9
  require 'rubocop/cop/betterment/spec_helper_required_outside_spec_dir'
6
10
  require 'rubocop/cop/betterment/implicit_redirect_type'
11
+ require 'rubocop/cop/betterment/active_job_performable'
12
+ require 'rubocop/cop/betterment/allowlist_blocklist'
13
+ require 'rubocop/cop/betterment/server_error_assertion'
@@ -0,0 +1,40 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class ActiveJobPerformable < Cop
5
+ MSG = <<-DOC.freeze
6
+ Classes that are "performable" should be ActiveJobs
7
+
8
+ class MyJob < ApplicationJob
9
+ def perform
10
+ end
11
+ end
12
+
13
+ You can learn more about ActiveJob here:
14
+ https://guides.rubyonrails.org/active_job_basics.html
15
+ DOC
16
+
17
+ def_node_matcher :subclasses_application_job?, <<-PATTERN
18
+ (class (const ...) (const _ :ApplicationJob) ...)
19
+ PATTERN
20
+
21
+ def_node_matcher :is_perform_method?, <<-PATTERN
22
+ (def :perform ...)
23
+ PATTERN
24
+
25
+ def on_class(node)
26
+ return unless has_perform_method?(node)
27
+ return if subclasses_application_job?(node)
28
+
29
+ add_offense(node.children.first)
30
+ end
31
+
32
+ private
33
+
34
+ def has_perform_method?(node)
35
+ node.descendants.find(&method(:is_perform_method?))
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # rubocop:disable Betterment/AllowlistBlocklist
2
+ module RuboCop
3
+ module Cop
4
+ module Betterment
5
+ class AllowlistBlocklist < Cop
6
+ MSG = <<-DOC.freeze
7
+ Betterment has moved away from usages of whitelist & blacklist in favor of more inclusive terms.
8
+ Replace any instances of these terms with more inclusive terms like allowlist, blocklist, denylist,
9
+ ignorelist, warnlist, safelist, etc. We generally use allowlist and blocklist and prefer consistency where
10
+ possible, but other terms may be more appropriate depending on your use case.
11
+
12
+ This is part of a larger initiative to replace exclusionary / harmful language and anti-bias tools and products.
13
+ DOC
14
+
15
+ def on_class(node)
16
+ evaluate_node(node)
17
+ end
18
+
19
+ private
20
+
21
+ def evaluate_node(node)
22
+ return unless should_use_allowlist?(node) || should_use_blocklist?(node)
23
+
24
+ add_offense(node)
25
+ end
26
+
27
+ def should_use_allowlist?(node)
28
+ node.to_s.downcase.include?('whitelist')
29
+ end
30
+
31
+ def should_use_blocklist?(node)
32
+ node.to_s.downcase.include?('blacklist')
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ # rubocop:enable Betterment/AllowlistBlocklist
@@ -0,0 +1,216 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class AuthorizationInController < Cop
5
+ attr_accessor :unsafe_parameters, :unsafe_regex
6
+
7
+ # MSG_UNSAFE_CREATE = 'Model created/updated using unsafe parameters'.freeze
8
+ MSG_UNSAFE_CREATE = <<~MSG.freeze
9
+ Model created/updated using unsafe parameters.
10
+ Please query for the associated record in a way that enforces authorization (e.g. "trust-root chaining"),
11
+ and then pass the resulting object into your model instead of the unsafe parameter.
12
+
13
+ INSTEAD OF THIS:
14
+ post_parameters = params.permit(:album_id, :caption)
15
+ Post.new(post_parameters)
16
+
17
+ DO THIS:
18
+ album = current_user.albums.find(params[:album_id])
19
+ post_parameters = params.permit(:caption).merge(album: album)
20
+ Post.new(post_parameters)
21
+
22
+ See here for more information on this error:
23
+ https://github.com/Betterment/rubocop-betterment/blob/master/README.md#bettermentauthorizationincontroller
24
+ MSG
25
+
26
+ def_node_matcher :model_new?, <<-PATTERN
27
+ (send (const ... _) {:new :build :create :create! :find_or_create_by :find_or_create_by! :find_or_initialize_by :find_or_initialize_by!} ...)
28
+ PATTERN
29
+
30
+ def_node_matcher :model_update?, <<-PATTERN
31
+ (send (...) {:assign_attributes :update :update! :find_or_create_by :find_or_create_by! :find_or_initialize_by :find_or_initialize_by! :update_attribute :update_attributes :update_attributes! :update_all :update_column :update_columns} ...)
32
+ PATTERN
33
+
34
+ def initialize(config = nil, options = nil)
35
+ super(config, options)
36
+ config = @config.for_cop(self)
37
+ @unsafe_parameters = config.fetch("unsafe_parameters", []).map(&:to_sym)
38
+ @unsafe_regex = Regexp.new config.fetch("unsafe_regex", ".*_id$")
39
+ @wrapper_methods = {}
40
+ @wrapper_names = []
41
+ end
42
+
43
+ def on_class(node)
44
+ track_methods(node)
45
+ track_assignments(node)
46
+ end
47
+
48
+ def on_send(node) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
49
+ _receiver_node, _method_name, *arg_nodes = *node
50
+
51
+ return if !model_new?(node) && !model_update?(node)
52
+
53
+ arg_nodes.each do |argument|
54
+ if argument.send_type?
55
+ tag_unsafe_param_hash(argument)
56
+ tag_unsafe_param_permit_wrapper(argument)
57
+ elsif argument.variable?
58
+ tag_unsafe_param_permit_wrapper(argument)
59
+ elsif argument.hash_type?
60
+ argument.children.each do |pair|
61
+ next if pair.type != :pair
62
+
63
+ _key, value = *pair.children
64
+ tag_unsafe_param_hash(value)
65
+ tag_unsafe_param_permit_wrapper(value)
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def tag_unsafe_param_hash(node)
74
+ unsafe_param = extract_parameter(node)
75
+ add_offense(unsafe_param, message: MSG_UNSAFE_CREATE) if unsafe_param
76
+ end
77
+
78
+ def tag_unsafe_param_permit_wrapper(node)
79
+ return if !node.send_type? && !node.variable?
80
+ return if node.send_type? && (node.method_name == :[])
81
+
82
+ name = Utils::Parser.get_root_token(node)
83
+ add_offense(node, message: MSG_UNSAFE_CREATE) if @wrapper_names.include?(name)
84
+ end
85
+
86
+ # if a method returns params[...] or params.permit(...) then it will
87
+ # be tracked; if the method does not explicitly return a params
88
+ # sourced argument, then it will not be tracked
89
+ def track_methods(node)
90
+ methods = get_all_methods(node)
91
+
92
+ methods.map do |method|
93
+ method_returns = Utils::Parser.get_return_values(method)
94
+
95
+ unless get_param_wrappers(method_returns).empty?
96
+ @wrapper_methods[method.method_name] = method
97
+ @wrapper_names << method.method_name
98
+ end
99
+ end
100
+ end
101
+
102
+ # keep track of all assignments that hold parameters
103
+ def track_assignments(node)
104
+ get_all_assignments(node).each do |assignment|
105
+ variable_name, value = *assignment
106
+
107
+ # if rhs calls params.permit, eg
108
+ # - @var = params.permit(...)
109
+ rhs_wrapper = get_param_wrappers([value])
110
+ unless rhs_wrapper.empty?
111
+ @wrapper_methods[variable_name] = rhs_wrapper[0]
112
+ @wrapper_names << variable_name
113
+ end
114
+
115
+ # if rhs is a call to a parameter wrapper, eg
116
+ # @var = parameter_wrapper
117
+ root_token = Utils::Parser.get_root_token(value)
118
+ if root_token && @wrapper_names.include?(root_token)
119
+ @wrapper_methods[variable_name] = assignment
120
+ @wrapper_names << variable_name
121
+ end
122
+
123
+ # if rhs extracts a parameter, eg
124
+ # @var = params[:user_id]
125
+ if extract_parameter(value)
126
+ @wrapper_methods[variable_name] = assignment
127
+ @wrapper_names << variable_name
128
+ end
129
+ end
130
+ end
131
+
132
+ def get_all_assignments(node)
133
+ return [] unless node.children && node.class_type?
134
+
135
+ node.descendants.select do |descendant|
136
+ _lhs, rhs = *descendant
137
+ descendant.equals_asgn? && (descendant.type != :casgn) && rhs&.send_type?
138
+ end
139
+ end
140
+
141
+ def param_symbol?(name)
142
+ name == :params
143
+ end
144
+
145
+ def get_all_methods(node)
146
+ return [] unless node.children && node.class_type?
147
+
148
+ node.descendants.select do |descendant|
149
+ descendant.def_type?
150
+ end
151
+ end
152
+
153
+ # this finds all calls to any method on a params like object
154
+ # then walks up to find calls to permit
155
+ # if the arguments to permit are "suspicious", then we add
156
+ # the whole method to a list of methods that wrap params.permit
157
+ def get_param_wrappers(methods) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
158
+ wrappers = []
159
+
160
+ methods.each do |method|
161
+ return [] unless method.def_type? || method.send_type?
162
+
163
+ method.descendants.each do |child|
164
+ next unless child.send_type?
165
+ next unless param_symbol?(child.method_name)
166
+
167
+ child.ancestors.each do |ancestor|
168
+ _receiver_node, method_name, *arg_nodes = *ancestor
169
+ next unless ancestor.send_type?
170
+ next unless method_name == :permit
171
+
172
+ # we're only interested in calls to params.....permit(...)
173
+ wrappers << method if contains_id_param?(arg_nodes)
174
+ end
175
+ end
176
+ end
177
+
178
+ wrappers
179
+ end
180
+
181
+ def sym_or_str?(arg)
182
+ %i(sym str).include?(arg.type)
183
+ end
184
+
185
+ def array_or_hash?(arg)
186
+ %i(array hash).include?(arg.type)
187
+ end
188
+
189
+ def contains_id_param?(arg_nodes)
190
+ arg_nodes.any? do |arg|
191
+ sym_or_str?(arg) && suspicious_id?(arg.value) ||
192
+ array_or_hash?(arg) && contains_id_param?(arg.values)
193
+ end
194
+ end
195
+
196
+ # check a symbol name against the cop's config parameters
197
+ def suspicious_id?(symbol_name)
198
+ @unsafe_parameters.include?(symbol_name.to_sym) || @unsafe_regex.match(symbol_name) # symbol_name.to_s.end_with?("_id")
199
+ end
200
+
201
+ def extract_parameter(argument)
202
+ _receiver_node, method_name, *arg_nodes = *argument
203
+ return unless argument.send_type? && method_name == :[]
204
+
205
+ argument_name = Utils::Parser.get_root_token(argument)
206
+
207
+ if param_symbol?(argument_name) || @wrapper_names.include?(argument_name)
208
+ arg_nodes.find do |arg|
209
+ sym_or_str?(arg) && suspicious_id?(arg.value)
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,36 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class DynamicParams < Cop
5
+ MSG_DYNAMIC_PARAMS = <<~MSG.freeze
6
+ Parameter names accessed dynamically, cannot determine safeness. Please inline the keys explicitly when calling `permit` or when accessing `params` like a hash.
7
+
8
+ See here for more information on this error:
9
+ https://github.com/Betterment/rubocop-betterment/blob/master/README.md#bettermentdynamicparams
10
+ MSG
11
+
12
+ def_node_matcher :permit_or_hash?, <<-PATTERN
13
+ (send (...) {:[] :permit} ...)
14
+ PATTERN
15
+
16
+ def on_send(node)
17
+ _, _, *arg_nodes = *node
18
+ return unless permit_or_hash?(node) && Utils::Parser.get_root_token(node) == :params
19
+
20
+ dynamic_param = find_dynamic_param(arg_nodes)
21
+ add_offense(dynamic_param, message: MSG_DYNAMIC_PARAMS) if dynamic_param
22
+ end
23
+
24
+ private
25
+
26
+ def find_dynamic_param(arg_nodes)
27
+ return unless arg_nodes
28
+
29
+ arg_nodes.find do |arg|
30
+ arg.array_type? && find_dynamic_param(arg.values) || !arg.literal? && !arg.const_type?
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -47,7 +47,7 @@ module RuboCop
47
47
  return unless routes_file?
48
48
 
49
49
  if block_form_with_options(node) { |options| options.none?(&method(:valid_status_option?)) } || block_form_without_options?(node)
50
- add_offense(node, message: MSG)
50
+ add_offense(node)
51
51
  end
52
52
  end
53
53
 
@@ -55,7 +55,7 @@ module RuboCop
55
55
  return unless routes_file?
56
56
 
57
57
  if arg_form_with_options(node) { |options| options.none?(&method(:valid_status_option?)) } || arg_form_without_options?(node)
58
- add_offense(node, message: MSG)
58
+ add_offense(node)
59
59
  end
60
60
  end
61
61
 
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Betterment
6
+ # Checks the status passed to have_http_status
7
+ #
8
+ # If a number, enforces that it doesn't start with 5. If a symbol or a string, enforces that it's not one of:
9
+ #
10
+ # * internal_server_error
11
+ # * not_implemented
12
+ # * bad_gateway
13
+ # * service_unavailable
14
+ # * gateway_timeout
15
+ # * http_version_not_supported
16
+ # * insufficient_storage
17
+ # * not_extended
18
+ #
19
+ # @example
20
+ #
21
+ # # bad
22
+ # expect(response).to have_http_status :internal_server_error
23
+ #
24
+ # # bad
25
+ # expect(response).to have_http_status 500
26
+ #
27
+ # # good
28
+ # expect(response).to have_http_status :forbidden
29
+ #
30
+ # # good
31
+ # expect(response).to have_http_status 422
32
+ class ServerErrorAssertion < Cop
33
+ MSG = 'Do not assert on 5XX statuses. Use a semantic status (e.g., 403, 422, etc.) or treat them as bugs (omit tests).'
34
+ BAD_STATUSES = %i(
35
+ internal_server_error
36
+ not_implemented
37
+ bad_gateway
38
+ service_unavailable
39
+ gateway_timeout
40
+ http_version_not_supported
41
+ insufficient_storage
42
+ not_extended
43
+ ).freeze
44
+
45
+ def_node_matcher :offensive_node?, <<-PATTERN
46
+ (send nil? :have_http_status
47
+ {
48
+ (int {#{(500..599).map(&:to_s).join(' ')}})
49
+ (str {#{BAD_STATUSES.map(&:to_s).map(&:inspect).join(' ')}})
50
+ (sym {#{BAD_STATUSES.map(&:inspect).join(' ')}})
51
+ }
52
+ )
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ return unless offensive_node?(node)
57
+
58
+ add_offense(node)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -21,7 +21,7 @@ module RuboCop
21
21
  PATTERN
22
22
 
23
23
  def on_send(node)
24
- add_offense(node, message: MSG) if requires_spec_helper?(node) && !spec_directory?
24
+ add_offense(node) if requires_spec_helper?(node) && !spec_directory?
25
25
  end
26
26
 
27
27
  private
@@ -0,0 +1,75 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class UnscopedFind < Cop
5
+ attr_accessor :unauthenticated_models
6
+
7
+ MSG = <<~MSG.freeze
8
+ Records are being retrieved directly using user input.
9
+ Please query for the associated record in a way that enforces authorization (e.g. "trust-root chaining").
10
+
11
+ INSTEAD OF THIS:
12
+ Post.find(params[:post_id])
13
+
14
+ DO THIS:
15
+ current_user.posts.find(params[:post_id])
16
+
17
+ See here for more information on this error:
18
+ https://github.com/Betterment/rubocop-betterment/blob/master/README.md#bettermentunscopedfind
19
+ MSG
20
+ METHOD_PATTERN = /^find_by_(.+?)(!)?$/.freeze
21
+ FINDS = %i(find find_by find_by! where).freeze
22
+
23
+ def_node_matcher :custom_scope_find?, <<-PATTERN
24
+ (send (send (const ... _) ...) {#{FINDS.map(&:inspect).join(' ')}} ...)
25
+ PATTERN
26
+
27
+ def_node_matcher :find?, <<-PATTERN
28
+ (send (const ... _) {#{FINDS.map(&:inspect).join(' ')}} ...)
29
+ PATTERN
30
+
31
+ def initialize(config = nil, options = nil)
32
+ super(config, options)
33
+ config = @config.for_cop(self)
34
+ @unauthenticated_models = config.fetch("unauthenticated_models", []).map(&:to_sym)
35
+ end
36
+
37
+ def on_send(node)
38
+ _, _, *arg_nodes = *node
39
+ return unless
40
+ (
41
+ find?(node) ||
42
+ custom_scope_find?(node) ||
43
+ static_method_name(node.method_name)
44
+ ) && !@unauthenticated_models.include?(Utils::Parser.get_root_token(node))
45
+
46
+ add_offense(node) if find_param_arg(arg_nodes)
47
+ end
48
+
49
+ private
50
+
51
+ def find_param_arg(arg_nodes)
52
+ return unless arg_nodes
53
+
54
+ arg_nodes.find do |arg|
55
+ if arg.hash_type?
56
+ arg.children.each do |pair|
57
+ _key, value = *pair.children
58
+ return arg if Utils::Parser.get_root_token(value) == :params
59
+ end
60
+ end
61
+ Utils::Parser.get_root_token(arg) == :params
62
+ end
63
+ end
64
+
65
+ # yoinked from Rails/DynamicFindBy
66
+ def static_method_name(method_name)
67
+ match = METHOD_PATTERN.match(method_name)
68
+ return nil unless match
69
+
70
+ match[2] ? 'find_by!' : 'find_by'
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,69 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Utils
4
+ module Parser
5
+ def self.get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity
6
+ return nil unless node
7
+
8
+ return get_root_token(node.receiver) if node.receiver
9
+
10
+ if node.send_type?
11
+ name = node.method_name
12
+ elsif node.variable?
13
+ name, = *node
14
+ elsif node.literal?
15
+ _, name = *node
16
+ elsif node.const_type?
17
+ name = node.const_name.to_sym
18
+ elsif node.sym_type?
19
+ name = node.value
20
+ elsif node.variable?
21
+ name = node.children[0]
22
+ elsif node.self_type?
23
+ name = :self
24
+ elsif node.block_pass_type?
25
+ name, = *node.children
26
+ else
27
+ name = nil
28
+ end
29
+
30
+ name
31
+ end
32
+
33
+ def self.get_return_values(node) # rubocop:disable Metrics/AbcSize
34
+ return [] unless node
35
+ return explicit_returns(node) + get_return_values(node.body) if node.def_type?
36
+ return [node] if node.literal? || node.variable?
37
+
38
+ case node.type
39
+ when :begin
40
+ get_return_values(node.children.last)
41
+ when :block
42
+ get_return_values(node.body)
43
+ when :if
44
+ if_rets = get_return_values(node.if_branch)
45
+ else_rets = get_return_values(node.else_branch)
46
+ if_rets + else_rets
47
+ when :case
48
+ cases = []
49
+ node.each_when do |block|
50
+ cases += get_return_values(block.body)
51
+ end
52
+
53
+ cases + get_return_values(node.else_branch)
54
+ when :send
55
+ [node]
56
+ else
57
+ []
58
+ end
59
+ end
60
+
61
+ def self.explicit_returns(node)
62
+ node.descendants.select(&:return_type?).map { |x|
63
+ x&.children&.first
64
+ }.compact
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-betterment
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Development
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-01 00:00:00.000000000 Z
11
+ date: 2020-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -91,16 +91,23 @@ files:
91
91
  - STYLEGUIDE.md
92
92
  - config/default.yml
93
93
  - lib/rubocop/cop/betterment.rb
94
+ - lib/rubocop/cop/betterment/active_job_performable.rb
95
+ - lib/rubocop/cop/betterment/allowlist_blocklist.rb
96
+ - lib/rubocop/cop/betterment/authorization_in_controller.rb
97
+ - lib/rubocop/cop/betterment/dynamic_params.rb
94
98
  - lib/rubocop/cop/betterment/implicit_redirect_type.rb
95
99
  - lib/rubocop/cop/betterment/memoization_with_arguments.rb
100
+ - lib/rubocop/cop/betterment/server_error_assertion.rb
96
101
  - lib/rubocop/cop/betterment/site_prism_loaded.rb
97
102
  - lib/rubocop/cop/betterment/spec_helper_required_outside_spec_dir.rb
98
103
  - lib/rubocop/cop/betterment/timeout.rb
99
- homepage:
104
+ - lib/rubocop/cop/betterment/unscoped_find.rb
105
+ - lib/rubocop/cop/betterment/utils/parser.rb
106
+ homepage:
100
107
  licenses:
101
108
  - MIT
102
109
  metadata: {}
103
- post_install_message:
110
+ post_install_message:
104
111
  rdoc_options: []
105
112
  require_paths:
106
113
  - lib
@@ -115,8 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
122
  - !ruby/object:Gem::Version
116
123
  version: '0'
117
124
  requirements: []
118
- rubygems_version: 3.1.3
119
- signing_key:
125
+ rubygems_version: 3.1.4
126
+ signing_key:
120
127
  specification_version: 4
121
128
  summary: Betterment rubocop configuration
122
129
  test_files: []