Resolving security issues with Rubocop

Sometimes when you are observing your project source code you can see code like:

Person.find(params[:id])

So, in simple Rails application it’s ok, there is no any problems, it works like a charm. But it can broke your businesses logic, so by passing in params id of person who doesn’t belong to your specific group you still will be able to explore his information. To avoid such problems you should use protected scope to find your person:

current_group.people.find(params[:id])

How to avoid such problems? Seems like it is an issue for static code analyzer, rubocop in our case.

Ok, let’s define code examples which should be reported:

Person.find(params[:id])
Person.includes(:assoc).find(params[:id])
Person.includes(:assoc).where(id: 27).find(params[:id])

Now we will use ruby-parse to observe AST of this parts:

;; Person.find(params[:id])
(send
  (const nil :Person) :find
  (send
    (lvar :params) :[]
    (sym :id)))

;; Person.includes(:assoc).find(params[:id])
(send
  (send
    (const nil :Person) :includes
    (sym :assoc)) :find
  (send
    (lvar :params) :[]
    (sym :id)))

;; Person.includes(:assoc).where(id: 27).find(params[:id])
(send
  (send
    (send
      (const nil :Person) :includes
      (sym :assoc)) :where
    (hash
      (pair
        (sym :id)
        (int 27)))) :find
  (send
    (lvar :params) :[]
    (sym :id)))

The third example it synthetic but we will use it just to illustrate concept. Now we are starting to write our cop:

module RuboCop
  class ProtectedScopeCop < RuboCop::Cop::Cop
  end
end

Now we need to catch AST node where we are calling :find method. Ruby-parser provides a list of methods which you can use to catch corresponding AST nodes. Each node in on_send method consists of thee parts [receiver, method, args]. So, firstly we need to filter non-find methods.

def on_send(node)
  _, method_name, _ = *node
  return unless method_name == :find
end

Easy enough. If we will explore all ASTs that we have we can detect pattern for calling :find on constants:

(send
  (send
    (send
      (const nil _) _ _) _ _)
      :find _)

Each level of (send ? _ _) is method calling on constant. In rubocop you can use #def_node_matcher. But there is no any special matcher to define optional nesting AST (at least I can find it in documentation. Ok, let’s simple flatten on node children.

def children(node)
  current_nodes = [node]
  while current_nodes.any? { |node| node.child_nodes.count != 0 } do
    current_nodes = current_nodes.flat_map { |node| node.child_nodes.count == 0 ? node : node.child_nodes }
  end
  current_nodes
end

It’s pretty simple, if this node has child – let’s retentive it! And now we need to check is there any constant node?

return unless children(node).any? { |node| node.type == :const }

Last action – add warning:

module RuboCop
  class ProtectedScopeCop < RuboCop::Cop::Cop
    def on_send(node)
      _, method_name, _ = *node
      return unless method_name == :find
      return unless children(node).any? { |node| node.type == :const }
      add_offense(node, :expression, "Dangerous Search!")
    end

    private

    def children(node)
      current_nodes = [node]
      while current_nodes.any? { |node| node.child_nodes.count != 0 } do
        current_nodes = current_nodes.flat_map { |node| node.child_nodes.count == 0 ? node : node.child_nodes }
      end
      current_nodes
    end
  end
end

It is good enough code which will help you to detect problems with unsafe searches. Some additional words:

  • You should add your cope to lib/
require:
  - ./protected_scope_cop.rb

RuboCop/ProtectedScopeCop:
  Include:
  - app/controllers/**/*.rb
  • You should resolve somehow cases when you really need to call find on all scope. For example
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token

How to deal with it? My suggest is to move constant to method which indicates that you are know what are you doing:

protected_scope.find(doorkeeper_token.resource_owner_id) if doorkeeper_token

def protected_scope
  User
end

That’s all.