Welcome to mirror list, hosted at ThFree Co, Russian Federation.

json_serialization.rb « security « cop « rubocop - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 50f2d73eee8239bbecf66a3dd847e231b22b3c31 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
module RuboCop
  module Cop
    module Security
      # This cop checks for `to_json`/`as_json` without `:only` options
      #
      # Either method called on an instance of a `Serializer` class will be
      # ignored. Associations included via `include` are subject to the same
      # rules.
      #
      # @example
      #
      #   # bad
      #   render json: @user.to_json
      #   render json: @user.to_json(except: %i[password])
      #   render json: @user.to_json(
      #     only: %i[username],
      #     include: { :identities }
      #   )
      #
      #   # acceptable
      #   render json: UserSerializer.new.to_json
      #
      #   # good
      #   render json: @user.to_json(only: %i[name username])
      #   render json: @user.to_json(include: {
      #     identities: { only: %i[provider] }
      #   })
      class JsonSerialization < RuboCop::Cop::Cop
        MSG = "Don't use `%s` without specifying `only`".freeze

        # Check for `to_json` sent to any object that's not a Hash literal or
        # Serializer instance
        def_node_matcher :to_json?, <<~PATTERN
          (send !{nil hash #serializer?} ${:to_json :as_json} $...)
        PATTERN

        # Check if node is a `only: ...` pair
        def_node_matcher :only_pair?, <<~PATTERN
          (pair (sym :only) ...)
        PATTERN

        # Check if node is a `include: {...}` pair
        def_node_matcher :include_pair?, <<~PATTERN
          (pair (sym :include) (hash $...))
        PATTERN

        # Check for a `only: [...]` pair anywhere in the node
        def_node_search :contains_only?, <<~PATTERN
          (pair (sym :only) (array ...))
        PATTERN

        # Check for `SomeConstant.new`
        def_node_search :constant_init, <<~PATTERN
          (send (const nil $_) :new)
        PATTERN

        def on_send(node)
          matched = to_json?(node)
          return unless matched

          @_has_top_level_only = false
          @method = matched.first

          if matched.last.nil? || matched.last.empty?
            # Empty `to_json` call
            add_offense(node, :expression, format_message)
          else
            options = matched.last.first

            # If `to_json` was given an argument that isn't a Hash, we don't
            # know what to do here, so just move along
            return unless options.hash_type?

            options.each_child_node do |child_node|
              check_pair(child_node)
            end

            # Add a top-level offense for the entire argument list, but only if
            # we haven't yet added any offenses to the child Hash values (such
            # as `include`)
            if requires_only?
              add_offense(node.children.last, :expression, format_message)
            end
          end
        end

        private

        def format_message
          format(MSG, @method)
        end

        def serializer?(node)
          constant_init(node).any? { |name| name.to_s.end_with?('Serializer') }
        end

        def check_pair(pair)
          if only_pair?(pair)
            @_has_top_level_only = true
          elsif include_pair?(pair)
            includes = pair.value

            includes.each_child_node do |child_node|
              next if contains_only?(child_node)

              add_offense(child_node, :expression, format_message)
            end
          end
        end

        def requires_only?
          return false if @_has_top_level_only

          offenses.count.zero?
        end
      end
    end
  end
end