rubocop-betterment 1.13.0 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +95 -4
- data/config/default.yml +4 -0
- data/lib/rubocop/cop/betterment.rb +3 -0
- data/lib/rubocop/cop/betterment/authorization_in_controller.rb +280 -0
- data/lib/rubocop/cop/betterment/dynamic_params.rb +62 -0
- data/lib/rubocop/cop/betterment/unscoped_find.rb +101 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72ecfa7e731527b0864a131730eb79829789b2b34647dd73ed6b17bdc5c5e630
|
4
|
+
data.tar.gz: 5337dbddc978a69778f40dde95c9136e83df54bf90e5b5eb122aaa925cf14e97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a1a2417233fd6494e0e4a99d4423fe0c1b051aaeb6e54cafe23791622f612afb48d169f06a4d23911a614fb19e937f3862743141915facc4bfdfbe64dd6bcd0
|
7
|
+
data.tar.gz: f0beed1cf45c911d169061c703db710d05400da4ddc13cf16151338668bb023df9b98fe311e863caca8fa4979f099ab054ccf8c40eebac070ed52f6bf6a0135a
|
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.
|
data/config/default.yml
CHANGED
@@ -17,6 +17,10 @@ AllCops:
|
|
17
17
|
DisplayStyleGuide: true
|
18
18
|
DisplayCopNames: true
|
19
19
|
|
20
|
+
Betterment/AuthorizationInController:
|
21
|
+
Description: 'Detects unsafe handling of id-like parameters in controllers.'
|
22
|
+
Enabled: false
|
23
|
+
|
20
24
|
Betterment/SitePrismLoaded:
|
21
25
|
Include:
|
22
26
|
- 'spec/features/**/*_spec.rb'
|
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'rubocop'
|
2
|
+
require 'rubocop/cop/betterment/authorization_in_controller'
|
3
|
+
require 'rubocop/cop/betterment/dynamic_params'
|
4
|
+
require 'rubocop/cop/betterment/unscoped_find'
|
2
5
|
require 'rubocop/cop/betterment/timeout'
|
3
6
|
require 'rubocop/cop/betterment/memoization_with_arguments'
|
4
7
|
require 'rubocop/cop/betterment/site_prism_loaded'
|
@@ -0,0 +1,280 @@
|
|
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.type == :send
|
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.type == :hash
|
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 = 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 = 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 = 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 explicit_returns(node)
|
133
|
+
node.descendants.select(&:return_type?).map { |x|
|
134
|
+
x&.children&.first
|
135
|
+
}.compact
|
136
|
+
end
|
137
|
+
|
138
|
+
def get_return_values(node) # rubocop:disable Metrics/AbcSize
|
139
|
+
return [] unless node
|
140
|
+
return explicit_returns(node) + get_return_values(node.body) if node.def_type?
|
141
|
+
return [node] if node.literal? || node.variable?
|
142
|
+
|
143
|
+
case node.type
|
144
|
+
when :begin
|
145
|
+
get_return_values(node.children.last)
|
146
|
+
when :block
|
147
|
+
get_return_values(node.body)
|
148
|
+
when :if
|
149
|
+
if_rets = get_return_values(node.if_branch)
|
150
|
+
else_rets = get_return_values(node.else_branch)
|
151
|
+
if_rets + else_rets
|
152
|
+
when :case
|
153
|
+
cases = []
|
154
|
+
node.each_when do |block|
|
155
|
+
cases += get_return_values(block.body)
|
156
|
+
end
|
157
|
+
|
158
|
+
cases + get_return_values(node.else_branch)
|
159
|
+
when :send
|
160
|
+
[node]
|
161
|
+
else
|
162
|
+
[]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def get_all_assignments(node)
|
167
|
+
return [] unless node.children && node.type == :class
|
168
|
+
|
169
|
+
node.descendants.select do |descendant|
|
170
|
+
_lhs, rhs = *descendant
|
171
|
+
descendant.equals_asgn? && (descendant.type != :casgn) && rhs&.send_type?
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def param_symbol?(name)
|
176
|
+
name == :params
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_all_methods(node)
|
180
|
+
return [] unless node.children && node.type == :class
|
181
|
+
|
182
|
+
node.descendants.select do |descendant|
|
183
|
+
descendant.type == :def
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# fetches the name of the leftmost ("root") token
|
188
|
+
# @vars.merge(this: that).merge(etc: etc) => @vars
|
189
|
+
# params.permit(:this) => params
|
190
|
+
# params[:field] => params
|
191
|
+
def get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
|
192
|
+
return nil unless node
|
193
|
+
|
194
|
+
return get_root_token(node.receiver) if node.receiver
|
195
|
+
|
196
|
+
if node.send_type?
|
197
|
+
name = node.method_name
|
198
|
+
elsif node.variable?
|
199
|
+
name, = *node
|
200
|
+
elsif node.literal?
|
201
|
+
_, name = *node
|
202
|
+
elsif node.const_type?
|
203
|
+
name = node.const_name
|
204
|
+
elsif node.sym_type?
|
205
|
+
name = node.value
|
206
|
+
elsif node.variable?
|
207
|
+
name = node.children[0]
|
208
|
+
elsif node.parenthesized_call?
|
209
|
+
name = nil
|
210
|
+
else
|
211
|
+
raise "Unknown node type: #{node.type.inspect}"
|
212
|
+
end
|
213
|
+
|
214
|
+
name
|
215
|
+
end
|
216
|
+
|
217
|
+
# this finds all calls to any method on a params like object
|
218
|
+
# then walks up to find calls to permit
|
219
|
+
# if the arguments to permit are "suspicious", then we add
|
220
|
+
# the whole method to a list of methods that wrap params.permit
|
221
|
+
def get_param_wrappers(methods) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
|
222
|
+
wrappers = []
|
223
|
+
|
224
|
+
methods.each do |method|
|
225
|
+
return [] unless method.type == :def || method.type == :send
|
226
|
+
|
227
|
+
method.descendants.each do |child|
|
228
|
+
next unless child.type == :send
|
229
|
+
next unless param_symbol?(child.method_name)
|
230
|
+
|
231
|
+
child.ancestors.each do |ancestor|
|
232
|
+
_receiver_node, method_name, *arg_nodes = *ancestor
|
233
|
+
next unless ancestor.send_type?
|
234
|
+
next unless method_name == :permit
|
235
|
+
|
236
|
+
# we're only interested in calls to params.....permit(...)
|
237
|
+
wrappers << method if contains_id_param?(arg_nodes)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
wrappers
|
243
|
+
end
|
244
|
+
|
245
|
+
def sym_or_str?(arg)
|
246
|
+
%i(sym str).include?(arg.type)
|
247
|
+
end
|
248
|
+
|
249
|
+
def array_or_hash?(arg)
|
250
|
+
%i(array hash).include?(arg.type)
|
251
|
+
end
|
252
|
+
|
253
|
+
def contains_id_param?(arg_nodes)
|
254
|
+
arg_nodes.any? do |arg|
|
255
|
+
sym_or_str?(arg) && suspicious_id?(arg.value) ||
|
256
|
+
array_or_hash?(arg) && contains_id_param?(arg.values)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# check a symbol name against the cop's config parameters
|
261
|
+
def suspicious_id?(symbol_name)
|
262
|
+
@unsafe_parameters.include?(symbol_name.to_sym) || @unsafe_regex.match(symbol_name) # symbol_name.to_s.end_with?("_id")
|
263
|
+
end
|
264
|
+
|
265
|
+
def extract_parameter(argument)
|
266
|
+
_receiver_node, method_name, *arg_nodes = *argument
|
267
|
+
return unless argument.send_type? && method_name == :[]
|
268
|
+
|
269
|
+
argument_name = get_root_token(argument)
|
270
|
+
|
271
|
+
if param_symbol?(argument_name) || @wrapper_names.include?(argument_name)
|
272
|
+
arg_nodes.find do |arg|
|
273
|
+
sym_or_str?(arg) && suspicious_id?(arg.value)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,62 @@
|
|
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) && 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.type == :array && find_dynamic_param(arg.values) || !arg.literal? && !arg.const_type?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
|
35
|
+
return nil unless node
|
36
|
+
|
37
|
+
return get_root_token(node.receiver) if node.receiver
|
38
|
+
|
39
|
+
if node.send_type?
|
40
|
+
name = node.method_name
|
41
|
+
elsif node.variable?
|
42
|
+
name, = *node
|
43
|
+
elsif node.literal?
|
44
|
+
_, name = *node
|
45
|
+
elsif node.const_type?
|
46
|
+
name = node.const_name
|
47
|
+
elsif node.sym_type?
|
48
|
+
name = node.value
|
49
|
+
elsif node.variable?
|
50
|
+
name = node.children[0]
|
51
|
+
elsif node.parenthesized_call?
|
52
|
+
name = nil
|
53
|
+
else
|
54
|
+
raise "Unknown node type: #{node.type.inspect}"
|
55
|
+
end
|
56
|
+
|
57
|
+
name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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", [])
|
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?(get_root_token(node))
|
45
|
+
|
46
|
+
add_offense(node, message: MSG) 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.type == :hash
|
56
|
+
arg.children.each do |pair|
|
57
|
+
_key, value = *pair.children
|
58
|
+
return arg if get_root_token(value) == :params
|
59
|
+
end
|
60
|
+
end
|
61
|
+
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
|
+
|
73
|
+
def get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
|
74
|
+
return nil unless node
|
75
|
+
|
76
|
+
return get_root_token(node.receiver) if node.receiver
|
77
|
+
|
78
|
+
if node.send_type?
|
79
|
+
name = node.method_name
|
80
|
+
elsif node.variable?
|
81
|
+
name, = *node
|
82
|
+
elsif node.literal?
|
83
|
+
_, name = *node
|
84
|
+
elsif node.const_type?
|
85
|
+
name = node.const_name
|
86
|
+
elsif node.sym_type?
|
87
|
+
name = node.value
|
88
|
+
elsif node.variable?
|
89
|
+
name = node.children[0]
|
90
|
+
elsif node.parenthesized_call?
|
91
|
+
name = nil
|
92
|
+
else
|
93
|
+
raise "Unknown node type: #{node.type.inspect}"
|
94
|
+
end
|
95
|
+
|
96
|
+
name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubocop-betterment
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Development
|
@@ -91,11 +91,14 @@ files:
|
|
91
91
|
- STYLEGUIDE.md
|
92
92
|
- config/default.yml
|
93
93
|
- lib/rubocop/cop/betterment.rb
|
94
|
+
- lib/rubocop/cop/betterment/authorization_in_controller.rb
|
95
|
+
- lib/rubocop/cop/betterment/dynamic_params.rb
|
94
96
|
- lib/rubocop/cop/betterment/implicit_redirect_type.rb
|
95
97
|
- lib/rubocop/cop/betterment/memoization_with_arguments.rb
|
96
98
|
- lib/rubocop/cop/betterment/site_prism_loaded.rb
|
97
99
|
- lib/rubocop/cop/betterment/spec_helper_required_outside_spec_dir.rb
|
98
100
|
- lib/rubocop/cop/betterment/timeout.rb
|
101
|
+
- lib/rubocop/cop/betterment/unscoped_find.rb
|
99
102
|
homepage:
|
100
103
|
licenses:
|
101
104
|
- MIT
|
@@ -115,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
118
|
- !ruby/object:Gem::Version
|
116
119
|
version: '0'
|
117
120
|
requirements: []
|
118
|
-
rubygems_version: 3.1.
|
121
|
+
rubygems_version: 3.1.4
|
119
122
|
signing_key:
|
120
123
|
specification_version: 4
|
121
124
|
summary: Betterment rubocop configuration
|