Class: Gitlab::Json::StreamValidator

Inherits:
Oj::ScHandler
  • Object
show all
Defined in:
lib/gitlab/json/stream_validator.rb

Overview

See www.rubydoc.info/gems/oj/Oj/ScHandler for required methods

Constant Summary collapse

LimitExceededError =
Class.new(StandardError)
DepthLimitError =
Class.new(LimitExceededError)
ArraySizeLimitError =
Class.new(LimitExceededError)
ElementCountLimitError =
Class.new(LimitExceededError)
HashSizeLimitError =
Class.new(LimitExceededError)
BodySizeExceededError =
Class.new(LimitExceededError)
NUMERIC_REGEX =

Match integers, floats, and scientific notation with reasonable size limits Supports: 123, -123, 12.3, -12.3, 1.23e10, -1.23E-10 Limits: max 15 digits for integer part, max 15 digits for fractional part, max 3 digits for exponent

/\A[+-]?(?:\d{1,15}\.?\d{0,15}|\.\d{1,15})(?:[eE][+-]?\d{1,3})?\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ StreamValidator

Returns a new instance of StreamValidator.



39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/gitlab/json/stream_validator.rb', line 39

def initialize(options)
  @options = options
  @depth = 0
  @array_counts = {} # Track size by array object_id
  @hash_counts = {} # Track size by hash object_id
  @body_bytesize = 0
  @total_elements = 0
  @stack = []
  @result = nil
  @max_depth_reached = 0
  @max_array_count = 0
  @max_hash_count = 0
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



19
20
21
# File 'lib/gitlab/json/stream_validator.rb', line 19

def options
  @options
end

#resultObject (readonly)

Returns the value of attribute result.



19
20
21
# File 'lib/gitlab/json/stream_validator.rb', line 19

def result
  @result
end

Class Method Details

.user_facing_error_message(exception) ⇒ Object

We want to hide the limits configured, but still show what type



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/gitlab/json/stream_validator.rb', line 22

def self.user_facing_error_message(exception)
  case exception
  when ::Gitlab::Json::StreamValidator::DepthLimitError
    "Parameters nested too deeply"
  when ::Gitlab::Json::StreamValidator::ArraySizeLimitError
    "Array parameter too large"
  when ::Gitlab::Json::StreamValidator::HashSizeLimitError
    "Hash parameter too large"
  when ::Gitlab::Json::StreamValidator::ElementCountLimitError
    "Too many total parameters"
  when ::Gitlab::Json::StreamValidator::BodySizeExceededError
    "JSON body too large"
  else
    "Invalid JSON: limit exceeded"
  end
end

Instance Method Details

#add_value(value) ⇒ Object

Called for root values (when not in a hash or array)



170
171
172
173
# File 'lib/gitlab/json/stream_validator.rb', line 170

def add_value(value)
  increment_element_count!
  @result = value
end

#array_append(array, value) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/gitlab/json/stream_validator.rb', line 156

def array_append(array, value)
  increment_element_count!

  current_size = @array_counts[array.object_id] || 0 # rubocop:disable Lint/HashCompareByIdentity -- We want to track by object ID

  check_array_size!(current_size)

  array << value
  new_size = current_size + 1
  @array_counts[array.object_id] = new_size # rubocop:disable Lint/HashCompareByIdentity -- We want to track by object ID
  @max_array_count = [@max_array_count, new_size].max
end

#array_endObject

Called when an array ends



146
147
148
149
150
151
152
153
154
# File 'lib/gitlab/json/stream_validator.rb', line 146

def array_end
  @depth -= 1
  array = @stack.pop
  @array_counts.delete(array.object_id)

  @result = array if @stack.empty?

  array
end

#array_startObject

Called when an array starts



134
135
136
137
138
139
140
141
142
143
# File 'lib/gitlab/json/stream_validator.rb', line 134

def array_start
  check_depth!
  @depth += 1
  @max_depth_reached = [@max_depth_reached, @depth].max

  array = []
  @array_counts[array.object_id] = 0 # rubocop:disable Lint/HashCompareByIdentity -- We want to track by object ID
  @stack.push(array)
  array
end

#hash_endObject

Called when a hash ends



103
104
105
106
107
108
109
110
111
# File 'lib/gitlab/json/stream_validator.rb', line 103

def hash_end
  @depth -= 1
  hash = @stack.pop
  @hash_counts.delete(hash.object_id)

  @result = hash if @stack.empty?

  hash
end

#hash_key(key) ⇒ Object

Called for each key in a hash



114
115
116
117
118
# File 'lib/gitlab/json/stream_validator.rb', line 114

def hash_key(key)
  increment_element_count!

  key
end

#hash_set(hash, key, value) ⇒ Object

Called when a key/value pair is complete



121
122
123
124
125
126
127
128
129
130
131
# File 'lib/gitlab/json/stream_validator.rb', line 121

def hash_set(hash, key, value)
  increment_element_count!

  current_size = @hash_counts[hash.object_id] || 0 # rubocop:disable Lint/HashCompareByIdentity -- We want to track by object ID
  check_hash_size!(current_size)

  hash[key] = value
  new_size = current_size + 1
  @hash_counts[hash.object_id] = new_size # rubocop:disable Lint/HashCompareByIdentity -- We want to track by object ID
  @max_hash_count = [@max_hash_count, new_size].max
end

#hash_startObject

Called when a hash starts



91
92
93
94
95
96
97
98
99
100
# File 'lib/gitlab/json/stream_validator.rb', line 91

def hash_start
  check_depth!
  @depth += 1
  @max_depth_reached = [@max_depth_reached, @depth].max

  hash = {}
  @hash_counts[hash.object_id] = 0 # rubocop:disable Lint/HashCompareByIdentity -- We want to track by object ID
  @stack.push(hash)
  hash
end

#metadataObject

Returns metadata about the parsed JSON structure



176
177
178
179
180
181
182
183
184
# File 'lib/gitlab/json/stream_validator.rb', line 176

def 
  {
    body_bytesize: @body_bytesize,
    total_elements: @total_elements,
    max_array_count: @max_array_count,
    max_hash_count: @max_hash_count,
    max_depth: @max_depth_reached
  }
end

#validate!(body) ⇒ nil

This method validates the JSON input against configured size limits. It uses Oj’s streaming parser for efficient processing of large JSON documents while enforcing safety limits.

are exceeded

Examples:

Parse a simple JSON string

validator = Gitlab::Json::StreamValidator.new(max_json_size_bytes: 1024)
validator.validate!('{"key": "value"}') #=> nil

Handle size limit exceeded

validator = Gitlab::Json::StreamValidator.new(max_json_size_bytes: 10)
validator.validate!('{"very": "long json string"}')
# raises BodySizeExceededError

Parameters:

  • body (String)

    the JSON string to parse

Returns:

  • (nil)

    returns nil after successful validation or when skipping primitive values

Raises:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/gitlab/json/stream_validator.rb', line 72

def validate!(body)
  return if body.nil? || body.empty?

  @body_bytesize = body.bytesize

  check_body_size!

  # Oj.sc_parse does not handle primitive values (see https://github.com/ohler55/oj/issues/979)
  # so we need to handle them separately before calling the streaming parser
  return if %w[true false null].include?(body)
  return if quoted_string?(body)
  return if body.encoding == Encoding::UTF_8 && body.valid_encoding? && NUMERIC_REGEX.match?(body)

  ::Oj.sc_parse(self, body)

  nil
end