maxmind-db 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +3 -3
  4. data/lib/maxmind/db/decoder.rb +198 -63
  5. data/lib/maxmind/db/file_reader.rb +5 -0
  6. data/lib/maxmind/db/memory_reader.rb +18 -1
  7. data/lib/maxmind/db.rb +39 -5
  8. metadata +46 -79
  9. data/Gemfile +0 -5
  10. data/Gemfile.lock +0 -58
  11. data/README.dev.md +0 -4
  12. data/Rakefile +0 -15
  13. data/bin/mmdb-benchmark.rb +0 -64
  14. data/maxmind-db.gemspec +0 -28
  15. data/test/data/LICENSE +0 -4
  16. data/test/data/MaxMind-DB-spec.md +0 -574
  17. data/test/data/README.md +0 -4
  18. data/test/data/bad-data/README.md +0 -7
  19. data/test/data/bad-data/libmaxminddb/libmaxminddb-offset-integer-overflow.mmdb +0 -0
  20. data/test/data/bad-data/maxminddb-golang/cyclic-data-structure.mmdb +0 -0
  21. data/test/data/bad-data/maxminddb-golang/invalid-bytes-length.mmdb +0 -1
  22. data/test/data/bad-data/maxminddb-golang/invalid-data-record-offset.mmdb +0 -0
  23. data/test/data/bad-data/maxminddb-golang/invalid-map-key-length.mmdb +0 -0
  24. data/test/data/bad-data/maxminddb-golang/invalid-string-length.mmdb +0 -1
  25. data/test/data/bad-data/maxminddb-golang/metadata-is-an-uint128.mmdb +0 -1
  26. data/test/data/bad-data/maxminddb-golang/unexpected-bytes.mmdb +0 -0
  27. data/test/data/bad-data/maxminddb-python/bad-unicode-in-map-key.mmdb +0 -0
  28. data/test/data/perltidyrc +0 -12
  29. data/test/data/source-data/GeoIP2-Anonymous-IP-Test.json +0 -49
  30. data/test/data/source-data/GeoIP2-City-Test.json +0 -12885
  31. data/test/data/source-data/GeoIP2-Connection-Type-Test.json +0 -122
  32. data/test/data/source-data/GeoIP2-Country-Test.json +0 -15904
  33. data/test/data/source-data/GeoIP2-DensityIncome-Test.json +0 -14
  34. data/test/data/source-data/GeoIP2-Domain-Test.json +0 -452
  35. data/test/data/source-data/GeoIP2-Enterprise-Test.json +0 -745
  36. data/test/data/source-data/GeoIP2-ISP-Test.json +0 -12605
  37. data/test/data/source-data/GeoIP2-Precision-Enterprise-Test.json +0 -2205
  38. data/test/data/source-data/GeoIP2-Static-IP-Score-Test.json +0 -2132
  39. data/test/data/source-data/GeoIP2-User-Count-Test.json +0 -2837
  40. data/test/data/source-data/GeoLite2-ASN-Test.json +0 -4120
  41. data/test/data/source-data/GeoLite2-City-Test.json +0 -12804
  42. data/test/data/source-data/GeoLite2-Country-Test.json +0 -11280
  43. data/test/data/source-data/README +0 -15
  44. data/test/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb +0 -0
  45. data/test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb +0 -0
  46. data/test/data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb +0 -0
  47. data/test/data/test-data/GeoIP2-City-Test.mmdb +0 -0
  48. data/test/data/test-data/GeoIP2-Connection-Type-Test.mmdb +0 -0
  49. data/test/data/test-data/GeoIP2-Country-Test.mmdb +0 -0
  50. data/test/data/test-data/GeoIP2-DensityIncome-Test.mmdb +0 -0
  51. data/test/data/test-data/GeoIP2-Domain-Test.mmdb +0 -0
  52. data/test/data/test-data/GeoIP2-Enterprise-Test.mmdb +0 -0
  53. data/test/data/test-data/GeoIP2-ISP-Test.mmdb +0 -0
  54. data/test/data/test-data/GeoIP2-Precision-Enterprise-Test.mmdb +0 -0
  55. data/test/data/test-data/GeoIP2-Static-IP-Score-Test.mmdb +0 -0
  56. data/test/data/test-data/GeoIP2-User-Count-Test.mmdb +0 -0
  57. data/test/data/test-data/GeoLite2-ASN-Test.mmdb +0 -0
  58. data/test/data/test-data/GeoLite2-City-Test.mmdb +0 -0
  59. data/test/data/test-data/GeoLite2-Country-Test.mmdb +0 -0
  60. data/test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb +0 -0
  61. data/test/data/test-data/MaxMind-DB-string-value-entries.mmdb +0 -0
  62. data/test/data/test-data/MaxMind-DB-test-broken-pointers-24.mmdb +0 -0
  63. data/test/data/test-data/MaxMind-DB-test-broken-search-tree-24.mmdb +0 -0
  64. data/test/data/test-data/MaxMind-DB-test-decoder.mmdb +0 -0
  65. data/test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb +0 -0
  66. data/test/data/test-data/MaxMind-DB-test-ipv4-28.mmdb +0 -0
  67. data/test/data/test-data/MaxMind-DB-test-ipv4-32.mmdb +0 -0
  68. data/test/data/test-data/MaxMind-DB-test-ipv6-24.mmdb +0 -0
  69. data/test/data/test-data/MaxMind-DB-test-ipv6-28.mmdb +0 -0
  70. data/test/data/test-data/MaxMind-DB-test-ipv6-32.mmdb +0 -0
  71. data/test/data/test-data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  72. data/test/data/test-data/MaxMind-DB-test-mixed-24.mmdb +0 -0
  73. data/test/data/test-data/MaxMind-DB-test-mixed-28.mmdb +0 -0
  74. data/test/data/test-data/MaxMind-DB-test-mixed-32.mmdb +0 -0
  75. data/test/data/test-data/MaxMind-DB-test-nested.mmdb +0 -0
  76. data/test/data/test-data/MaxMind-DB-test-pointer-decoder.mmdb +0 -0
  77. data/test/data/test-data/README.md +0 -26
  78. data/test/data/test-data/maps-with-pointers.raw +0 -0
  79. data/test/data/test-data/write-test-data.pl +0 -695
  80. data/test/data/tidyall.ini +0 -5
  81. data/test/mmdb_util.rb +0 -26
  82. data/test/test_decoder.rb +0 -241
  83. data/test/test_reader.rb +0 -547
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b642dbfca79a5abf8c5c5ab4a2ceb07051affb7148dd005514d0bc1a9950c3b
4
- data.tar.gz: 6cb14299dec849aef6f054ca39cbf139d3e552ed593960ebfbd72a09a6ce5f27
3
+ metadata.gz: 19db0a99249703b86573eb824fe94f4eb4c807528699c7707b2b8ef7d22f89c9
4
+ data.tar.gz: efb878012b9565ffc0b7d9fde72b02685c85bbafe28819550ec184f88709640b
5
5
  SHA512:
6
- metadata.gz: 232d60717fed7da9d7253edc041316eec8f71782b9fca2e72cdbd5ad5414e7253504d423b3bab105cd87c5fd1c62e6df2fe7b8a2d70ea19f97a5fc5fc493a572
7
- data.tar.gz: c40ce6eec502c4f2f45d8b5036b179b9f498e680771571430228845f0a588a564e3a4adf66193f56440bb8c21940793cba5ee073f3eb444dbe9f303db9f6fe21
6
+ metadata.gz: b5b53cb8a651d3456fa8f0fa4f2ee40baf6a1cd4e80f732ae1476d7a28f7e17492a89073c75c081243b40f7760a4cfb1a362de7090a9ad5b03a0e3ccfdcec9a0
7
+ data.tar.gz: 897c4cb5a162a90b3481668ede6a366a4e598b7b392e536de5be57e73baa4a6f5515721806c3f6db402060a07e52207b166a8cfc459a4d84df12538e08c8f9ce
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0 (2026-09-10)
4
+
5
+ * Fixed two denial-of-service issues in the decoder. A crafted database could
6
+ nest data-section pointers to shared targets so that decoding one record
7
+ cost exponential time and memory from a small file, or point many times at
8
+ one large string or bytes value so that a record with few values
9
+ materialized gigabytes. The decoder now bounds each record it decodes and
10
+ the metadata decoded when a database is opened. A database that exceeds a
11
+ limit raises `InvalidDatabaseError`. The limits are:
12
+ * 65,536 decoded values, as the MaxMind DB specification recommends.
13
+ * 512 levels of nesting, as the specification recommends. This also stops
14
+ pointer cycles.
15
+ * 2 MiB of string, bytes, and integer payload. The specification leaves this
16
+ limit to the reader. 2 MiB matches libmaxminddb.
17
+ * The decoder limits can be changed with the new `max_values`,
18
+ `max_payload_bytes`, and `max_depth` options to `MaxMind::DB.new`.
19
+ * Pointers that target other pointers are now rejected as invalid, as required
20
+ by the MaxMind DB specification.
21
+ * Lookups are faster. The decoder allocates fewer strings and dispatches on the
22
+ data type with a jump table. GeoLite City lookups in memory mode on CRuby
23
+ 3.4 are about 18% faster than in 1.4.0.
24
+ * Unnecessary files were removed from the published .gem.
25
+
26
+ ## 1.4.0 (2025-11-20)
27
+
28
+ * Ruby 3.2+ is now required. If you're using Ruby 3.0 or 3.1, please use
29
+ version 1.3.2 of this gem.
30
+
3
31
  ## 1.3.2 (2025-04-03)
4
32
 
5
33
  * Re-release to fix a release script problem. There are no code changes.
data/README.md CHANGED
@@ -35,7 +35,7 @@ For more information see the
35
35
 
36
36
  ## Requirements
37
37
 
38
- This code requires Ruby version 3.0 or higher.
38
+ This code requires Ruby version 3.2 or higher.
39
39
 
40
40
  ## Contributing
41
41
 
@@ -48,7 +48,7 @@ Please report all issues with this code using the [GitHub issue
48
48
  tracker](https://github.com/maxmind/MaxMind-DB-Reader-ruby/issues).
49
49
 
50
50
  If you are having an issue with a MaxMind service that is not specific to the
51
- client API, please see [our support page](https://www.maxmind.com/en/support).
51
+ client API, please see [our support page](https://support.maxmind.com/knowledge-base).
52
52
 
53
53
  ## Versioning
54
54
 
@@ -56,7 +56,7 @@ This library uses [Semantic Versioning](https://semver.org/).
56
56
 
57
57
  ## Copyright and License
58
58
 
59
- This software is Copyright (c) 2018 - 2024 by MaxMind, Inc.
59
+ This software is Copyright (c) 2018 - 2026 by MaxMind, Inc.
60
60
 
61
61
  This is free software, licensed under the [Apache License, Version
62
62
  2.0](LICENSE-APACHE) or the [MIT License](LICENSE-MIT), at your option.
@@ -12,12 +12,12 @@ module MaxMind
12
12
  #
13
13
  # @!visibility private
14
14
  class Decoder
15
- # rubocop:disable Style/OptionalBooleanParameter
15
+ # rubocop:disable Style/OptionalBooleanParameter, Metrics/ParameterLists
16
16
 
17
17
  # Create a +Decoder+.
18
18
  #
19
- # +io+ is the DB. It must provide a +read+ method. It must be opened in
20
- # binary mode.
19
+ # +io+ is the DB. It must provide +read+ and +getbyte+ methods. It must be
20
+ # opened in binary mode.
21
21
  #
22
22
  # +pointer_base+ is the base number to use when decoding a pointer. It is
23
23
  # where the data section begins rather than the beginning of the file.
@@ -25,21 +25,102 @@ module MaxMind
25
25
  # section.
26
26
  #
27
27
  # +pointer_test+ is used for testing pointer code.
28
- def initialize(io, pointer_base = 0, pointer_test = false)
28
+ #
29
+ # +max_values+, +max_payload_bytes+, and +max_depth+ set the per-decode
30
+ # limits described below and default to the constants there.
31
+ def initialize(io, pointer_base = 0, pointer_test = false,
32
+ max_values: MAX_VALUES, max_payload_bytes: MAX_BYTES,
33
+ max_depth: MAX_DEPTH)
29
34
  @io = io
30
35
  @pointer_base = pointer_base
31
36
  @pointer_test = pointer_test
37
+ @max_values = max_values
38
+ @max_payload_bytes = max_payload_bytes
39
+ @max_depth = max_depth
32
40
  end
33
- # rubocop:enable Style/OptionalBooleanParameter
41
+ # rubocop:enable Style/OptionalBooleanParameter, Metrics/ParameterLists
42
+
43
+ # Per-decode limits. The value and depth limits are the ones the MaxMind DB
44
+ # specification recommends. The specification leaves the payload limit to
45
+ # the reader, and 2 MiB matches libmaxminddb. +budget+ is a three-element
46
+ # array, [values_remaining, depth, bytes_remaining], shared across the
47
+ # recursion so every count survives it. It is call-local, which keeps the
48
+ # decoder safe for concurrent reads.
49
+ #
50
+ # The value limit stops a pointer fan-out. It follows the specification's
51
+ # flat rule: the root is one value, each array reserves one value per
52
+ # element, and each map reserves two values per entry before iterating. A
53
+ # pointer is not charged separately from the logical value at the root or
54
+ # its position in a container. A re-decoded node drains the budget, and an
55
+ # oversized declared size is rejected before the loop reads anything. The
56
+ # largest real records decode a few hundred values.
57
+ #
58
+ # The byte limit stops payload amplification: a crafted database can point
59
+ # many times at one large string or bytes value, so a bounded value count
60
+ # still materializes gigabytes. Each string and bytes value, and each
61
+ # variable-length integer, subtracts its own length before it is read, so a
62
+ # re-decoded (fanned-out) target recharges its payload and an oversized
63
+ # declared length is rejected before any bytes are copied. Fixed-width
64
+ # scalars are not charged.
65
+ #
66
+ # The depth limit stops a pointer cycle or over-deep data before the stack
67
+ # overflows.
68
+ MAX_VALUES = 1 << 16
69
+ private_constant :MAX_VALUES
70
+
71
+ MAX_BYTES = 1 << 21
72
+ private_constant :MAX_BYTES
73
+
74
+ MAX_DEPTH = 512
75
+ private_constant :MAX_DEPTH
76
+
77
+ BUDGET_VALUES = 0
78
+ BUDGET_DEPTH = 1
79
+ BUDGET_BYTES = 2
80
+ private_constant :BUDGET_VALUES, :BUDGET_DEPTH, :BUDGET_BYTES
81
+
82
+ # JRuby can exhaust the stack before the depth limit is reached and raises
83
+ # a Java StackOverflowError, which is not a SystemStackError. Catch both so
84
+ # a pointer cycle always becomes an InvalidDatabaseError.
85
+ STACK_ERRORS = if defined?(JRUBY_VERSION)
86
+ [SystemStackError, Java::JavaLang::StackOverflowError].freeze
87
+ else
88
+ [SystemStackError].freeze
89
+ end
90
+ private_constant :STACK_ERRORS
34
91
 
35
92
  private
36
93
 
37
- def decode_array(size, offset)
94
+ # The limit checks are inlined at each call site so containers and
95
+ # pointers do not add a helper call. Only the raise is factored out.
96
+ def raise_depth_exceeded
97
+ raise InvalidDatabaseError,
98
+ 'The MaxMind DB file\'s data section exceeds the maximum depth'
99
+ end
100
+
101
+ def raise_values_exceeded
102
+ raise InvalidDatabaseError,
103
+ 'The MaxMind DB file\'s data section exceeds the maximum number of values'
104
+ end
105
+
106
+ # Each string, bytes, and variable-length integer decoder charges its size
107
+ # against the payload budget inline, before the bytes are read, so an
108
+ # oversized declared length is rejected before it is copied. Ruby integers
109
+ # are arbitrary precision, so the subtraction cannot overflow.
110
+ def raise_bytes_exceeded
111
+ raise InvalidDatabaseError,
112
+ 'The MaxMind DB file\'s data section exceeds the maximum number of bytes'
113
+ end
114
+
115
+ def decode_array(size, offset, budget)
116
+ raise_values_exceeded if (budget[BUDGET_VALUES] -= size) < 0
117
+ raise_depth_exceeded if (budget[BUDGET_DEPTH] += 1) > @max_depth
38
118
  array = []
39
119
  size.times do
40
- value, offset = decode(offset)
120
+ value, offset = decode_with_budget(offset, budget)
41
121
  array << value
42
122
  end
123
+ budget[BUDGET_DEPTH] -= 1
43
124
  [array, offset]
44
125
  end
45
126
 
@@ -47,7 +128,8 @@ module MaxMind
47
128
  [size != 0, offset]
48
129
  end
49
130
 
50
- def decode_bytes(size, offset)
131
+ def decode_bytes(size, offset, budget)
132
+ raise_bytes_exceeded if (budget[BUDGET_BYTES] -= size) < 0
51
133
  [@io.read(offset, size), offset + size]
52
134
  end
53
135
 
@@ -66,37 +148,45 @@ module MaxMind
66
148
  def verify_size(expected, actual)
67
149
  return if expected == actual
68
150
 
151
+ raise_invalid_size
152
+ end
153
+
154
+ def raise_invalid_size
69
155
  raise InvalidDatabaseError,
70
156
  'The MaxMind DB file\'s data section contains bad data (unknown data type or corrupt data)'
71
157
  end
72
158
 
73
- def decode_int32(size, offset)
74
- decode_int('l>', 4, size, offset)
159
+ def decode_int32(size, offset, budget)
160
+ decode_int('l>', 4, size, offset, budget)
75
161
  end
76
162
 
77
- def decode_uint16(size, offset)
78
- decode_int('n', 2, size, offset)
163
+ def decode_uint16(size, offset, budget)
164
+ decode_int('n', 2, size, offset, budget)
79
165
  end
80
166
 
81
- def decode_uint32(size, offset)
82
- decode_int('N', 4, size, offset)
167
+ def decode_uint32(size, offset, budget)
168
+ decode_int('N', 4, size, offset, budget)
83
169
  end
84
170
 
85
- def decode_uint64(size, offset)
86
- decode_int('Q>', 8, size, offset)
171
+ def decode_uint64(size, offset, budget)
172
+ decode_int('Q>', 8, size, offset, budget)
87
173
  end
88
174
 
89
- def decode_int(type_code, type_size, size, offset)
175
+ def decode_int(type_code, type_size, size, offset, budget)
176
+ raise_invalid_size if size > type_size
90
177
  return 0, offset if size == 0
91
178
 
179
+ raise_bytes_exceeded if (budget[BUDGET_BYTES] -= size) < 0
92
180
  buf = @io.read(offset, size)
93
181
  buf = buf.rjust(type_size, "\x00") if size != type_size
94
182
  [buf.unpack1(type_code), offset + size]
95
183
  end
96
184
 
97
- def decode_uint128(size, offset)
185
+ def decode_uint128(size, offset, budget)
186
+ raise_invalid_size if size > 16
98
187
  return 0, offset if size == 0
99
188
 
189
+ raise_bytes_exceeded if (budget[BUDGET_BYTES] -= size) < 0
100
190
  buf = @io.read(offset, size)
101
191
 
102
192
  if size <= 8
@@ -112,45 +202,49 @@ module MaxMind
112
202
  [a | b, offset + size]
113
203
  end
114
204
 
115
- def decode_map(size, offset)
205
+ def decode_map(size, offset, budget)
206
+ # A map entry decodes a key and a value, so it costs two values.
207
+ raise_values_exceeded if (budget[BUDGET_VALUES] -= size * 2) < 0
208
+ raise_depth_exceeded if (budget[BUDGET_DEPTH] += 1) > @max_depth
116
209
  container = {}
117
210
  size.times do
118
- key, offset = decode(offset)
119
- value, offset = decode(offset)
211
+ key, offset = decode_with_budget(offset, budget)
212
+ value, offset = decode_with_budget(offset, budget)
120
213
  container[key] = value
121
214
  end
215
+ budget[BUDGET_DEPTH] -= 1
122
216
  [container, offset]
123
217
  end
124
218
 
125
219
  def decode_pointer(size, offset)
126
220
  pointer_size = size >> 3
127
221
 
222
+ # Build the pointer with integer arithmetic to avoid temporary strings
223
+ # when combining control bits with the payload bytes.
128
224
  case pointer_size
129
225
  when 0
130
226
  new_offset = offset + 1
131
- buf = (size & 0x7).chr << @io.read(offset, 1)
132
- pointer = buf.unpack1('n') + @pointer_base
227
+ pointer = ((size & 0x7) << 8) | @io.getbyte(offset)
133
228
  when 1
134
229
  new_offset = offset + 2
135
- buf = "\x00".b << (size & 0x7).chr << @io.read(offset, 2)
136
- pointer = buf.unpack1('N') + 2048 + @pointer_base
230
+ pointer = ((size & 0x7) << 16) | @io.read(offset, 2).unpack1('n')
231
+ pointer += 2048
137
232
  when 2
138
233
  new_offset = offset + 3
139
- buf = (size & 0x7).chr << @io.read(offset, 3)
140
- pointer = buf.unpack1('N') + 526_336 + @pointer_base
234
+ buf = @io.read(offset, 3)
235
+ pointer = ((size & 0x7) << 24) | (buf.getbyte(0) << 16) |
236
+ (buf.getbyte(1) << 8) | buf.getbyte(2)
237
+ pointer += 526_336
141
238
  else
142
239
  new_offset = offset + 4
143
- buf = @io.read(offset, 4)
144
- pointer = buf.unpack1('N') + @pointer_base
240
+ pointer = @io.read(offset, 4).unpack1('N')
145
241
  end
146
-
147
- return pointer, new_offset if @pointer_test
148
-
149
- value, = decode(pointer)
150
- [value, new_offset]
242
+ pointer += @pointer_base
243
+ [pointer, new_offset]
151
244
  end
152
245
 
153
- def decode_utf8_string(size, offset)
246
+ def decode_utf8_string(size, offset, budget)
247
+ raise_bytes_exceeded if (budget[BUDGET_BYTES] -= size) < 0
154
248
  new_offset = offset + size
155
249
  buf = @io.read(offset, size)
156
250
  buf.force_encoding(Encoding::UTF_8)
@@ -159,23 +253,6 @@ module MaxMind
159
253
  [buf, new_offset]
160
254
  end
161
255
 
162
- TYPE_DECODER = {
163
- 1 => :decode_pointer,
164
- 2 => :decode_utf8_string,
165
- 3 => :decode_double,
166
- 4 => :decode_bytes,
167
- 5 => :decode_uint16,
168
- 6 => :decode_uint32,
169
- 7 => :decode_map,
170
- 8 => :decode_int32,
171
- 9 => :decode_uint64,
172
- 10 => :decode_uint128,
173
- 11 => :decode_array,
174
- 14 => :decode_boolean,
175
- 15 => :decode_float,
176
- }.freeze
177
- private_constant :TYPE_DECODER
178
-
179
256
  public
180
257
 
181
258
  # Decode a section of the data section starting at +offset+.
@@ -187,23 +264,82 @@ module MaxMind
187
264
  #
188
265
  # Throws an exception if there is an error.
189
266
  def decode(offset)
267
+ # Bound the work per decode so a crafted database cannot exhaust CPU or
268
+ # memory. +budget+ carries the remaining value count, the current depth,
269
+ # and the remaining payload-byte allowance, and is call-local, which
270
+ # keeps the decoder safe for concurrent reads. The root value is charged
271
+ # here; containers charge their children. The depth limit catches a
272
+ # pointer cycle on MRI. JRuby can exhaust the stack before the limit is
273
+ # reached and raises a Java StackOverflowError, so catch that too and
274
+ # report the same error.
275
+ decode_with_budget(offset, [@max_values - 1, 0, @max_payload_bytes])
276
+ rescue *STACK_ERRORS
277
+ raise InvalidDatabaseError,
278
+ 'The MaxMind DB file\'s data section exceeds the maximum depth'
279
+ end
280
+
281
+ private
282
+
283
+ # The dispatch below is one branch per data type, so the method's
284
+ # cyclomatic complexity is above the cop's default. It is inlined here
285
+ # for speed and the branches are uniform.
286
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
287
+ def decode_with_budget(offset, budget)
288
+ pointer_return_offset = nil
190
289
  new_offset = offset + 1
191
- buf = @io.read(offset, 1)
192
- ctrl_byte = buf.ord
290
+ ctrl_byte = @io.getbyte(offset)
193
291
  type_num = ctrl_byte >> 5
194
292
  type_num, new_offset = read_extended(new_offset) if type_num == 0
195
293
 
196
294
  size, new_offset = size_from_ctrl_byte(ctrl_byte, new_offset, type_num)
197
- # We could check an element exists at `type_num', but for performance I
198
- # don't.
199
- send(TYPE_DECODER[type_num], size, new_offset)
200
- end
295
+ if type_num == 1
296
+ pointer, pointer_return_offset = decode_pointer(size, new_offset)
297
+ return [pointer, pointer_return_offset] if @pointer_test
298
+
299
+ # The root or containing collection already charged the logical value
300
+ # at the pointer's position. Following it adds depth but no separate
301
+ # value. Its target cannot be another pointer, and decoding the target
302
+ # still reserves container children and charges payload bytes.
303
+ raise_depth_exceeded if (budget[BUDGET_DEPTH] += 1) > @max_depth
304
+ new_offset = pointer + 1
305
+ ctrl_byte = @io.getbyte(pointer)
306
+ type_num = ctrl_byte >> 5
307
+ if type_num == 1
308
+ raise InvalidDatabaseError,
309
+ 'The MaxMind DB file\'s data section contains bad data (pointer points to another pointer)'
310
+ end
311
+ type_num, new_offset = read_extended(new_offset) if type_num == 0
312
+ size, new_offset = size_from_ctrl_byte(ctrl_byte, new_offset, type_num)
313
+ end
201
314
 
202
- private
315
+ # Direct case dispatch avoids looking the method up in a Hash and
316
+ # calling it with send.
317
+ result = case type_num
318
+ when 2 then decode_utf8_string(size, new_offset, budget)
319
+ when 3 then decode_double(size, new_offset)
320
+ when 4 then decode_bytes(size, new_offset, budget)
321
+ when 5 then decode_uint16(size, new_offset, budget)
322
+ when 6 then decode_uint32(size, new_offset, budget)
323
+ when 7 then decode_map(size, new_offset, budget)
324
+ when 8 then decode_int32(size, new_offset, budget)
325
+ when 9 then decode_uint64(size, new_offset, budget)
326
+ when 10 then decode_uint128(size, new_offset, budget)
327
+ when 11 then decode_array(size, new_offset, budget)
328
+ when 14 then decode_boolean(size, new_offset)
329
+ when 15 then decode_float(size, new_offset)
330
+ else
331
+ raise InvalidDatabaseError,
332
+ "The MaxMind DB file's data section contains bad data (unknown data type #{type_num})"
333
+ end
334
+ return result unless pointer_return_offset
335
+
336
+ budget[BUDGET_DEPTH] -= 1
337
+ result[1] = pointer_return_offset
338
+ result
339
+ end
203
340
 
204
341
  def read_extended(offset)
205
- buf = @io.read(offset, 1)
206
- next_byte = buf.ord
342
+ next_byte = @io.getbyte(offset)
207
343
  type_num = next_byte + 7
208
344
  if type_num < 7
209
345
  raise InvalidDatabaseError,
@@ -218,8 +354,7 @@ module MaxMind
218
354
  return size, offset if type_num == 1 || size < 29
219
355
 
220
356
  if size == 29
221
- size_bytes = @io.read(offset, 1)
222
- size = 29 + size_bytes.ord
357
+ size = 29 + @io.getbyte(offset)
223
358
  return size, offset + 1
224
359
  end
225
360
 
@@ -44,6 +44,11 @@ module MaxMind
44
44
  @fh.close
45
45
  end
46
46
 
47
+ # Return the byte at +offset+ as an Integer.
48
+ def getbyte(offset)
49
+ read(offset, 1).ord
50
+ end
51
+
47
52
  def read(offset, size)
48
53
  return ''.b if size == 0
49
54
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'maxmind/db/errors'
4
+
3
5
  module MaxMind
4
6
  class DB
5
7
  # @!visibility private
@@ -24,8 +26,23 @@ module MaxMind
24
26
 
25
27
  def close; end
26
28
 
29
+ # Return the byte at +offset+ as an Integer without allocating a String.
30
+ def getbyte(offset)
31
+ @buf.getbyte(offset) || raise_bad_data
32
+ end
33
+
27
34
  def read(offset, size)
28
- @buf[offset, size]
35
+ return ''.b if size == 0
36
+
37
+ raise_bad_data if offset + size > @buf.length
38
+
39
+ @buf.byteslice(offset, size)
40
+ end
41
+
42
+ private
43
+
44
+ def raise_bad_data
45
+ raise InvalidDatabaseError, 'The MaxMind DB file contains bad data'
29
46
  end
30
47
  end
31
48
  end
data/lib/maxmind/db.rb CHANGED
@@ -72,7 +72,7 @@ module MaxMind
72
72
  # @param database [String] a path to a {MaxMind
73
73
  # DB}[https://maxmind.github.io/MaxMind-DB/].
74
74
  #
75
- # @param options [Hash<Symbol, Symbol>] options controlling the behavior of
75
+ # @param options [Hash<Symbol, Object>] options controlling the behavior of
76
76
  # the DB.
77
77
  #
78
78
  # @option options [Symbol] :mode Defines how to open the database. It may
@@ -80,11 +80,27 @@ module MaxMind
80
80
  # one, DB uses MODE_AUTO. Refer to the definition of those constants for
81
81
  # an explanation of their meaning.
82
82
  #
83
- # @raise [InvalidDatabaseError] if the database is corrupt or invalid.
83
+ # @option options [Integer] :max_values The maximum number of values a
84
+ # single record, or the metadata, may decode to. The default is 65,536.
85
+ # The largest records MaxMind produces decode to a few hundred values.
86
+ #
87
+ # @option options [Integer] :max_payload_bytes The maximum total size in
88
+ # bytes of the strings, bytes, and integers a single record, or the
89
+ # metadata, may decode. The default is 2 MiB. The largest records MaxMind
90
+ # produces hold about a kilobyte.
91
+ #
92
+ # @option options [Integer] :max_depth The maximum nesting depth of maps,
93
+ # arrays, and pointers in a single record, or the metadata. The default
94
+ # is 512.
84
95
  #
85
- # @raise [ArgumentError] if the mode is invalid.
96
+ # @raise [InvalidDatabaseError] if the database is corrupt or invalid. A
97
+ # database that exceeds any of the limits above raises this error from
98
+ # the lookup, or from this constructor if the metadata exceeds them.
99
+ #
100
+ # @raise [ArgumentError] if the mode or a limit is invalid.
86
101
  def initialize(database, options = {})
87
102
  options[:mode] = MODE_AUTO unless options.key?(:mode)
103
+ limits = decoder_limits(options)
88
104
 
89
105
  case options[:mode]
90
106
  when MODE_AUTO, MODE_FILE
@@ -101,11 +117,11 @@ module MaxMind
101
117
  @size = @io.size
102
118
 
103
119
  metadata_start = find_metadata_start
104
- metadata_decoder = Decoder.new(@io, metadata_start)
120
+ metadata_decoder = Decoder.new(@io, metadata_start, **limits)
105
121
  metadata_map, = metadata_decoder.decode(metadata_start)
106
122
  @metadata = Metadata.new(metadata_map)
107
123
  @decoder = Decoder.new(@io, @metadata.search_tree_size +
108
- DATA_SECTION_SEPARATOR_SIZE)
124
+ DATA_SECTION_SEPARATOR_SIZE, **limits)
109
125
 
110
126
  # Store copies as instance variables to reduce method calls.
111
127
  @ip_version = @metadata.ip_version
@@ -271,6 +287,24 @@ module MaxMind
271
287
  data
272
288
  end
273
289
 
290
+ LIMIT_OPTIONS = %i[max_values max_payload_bytes max_depth].freeze
291
+ private_constant :LIMIT_OPTIONS
292
+
293
+ # Return the decoder limits given in +options+ as keyword arguments for
294
+ # Decoder.new. An absent option keeps the decoder's default.
295
+ def decoder_limits(options)
296
+ limits = {}
297
+ LIMIT_OPTIONS.each do |name|
298
+ next unless options.key?(name)
299
+
300
+ value = options[name]
301
+ raise ArgumentError, "#{name} must be a positive integer" unless value.is_a?(Integer) && value.positive?
302
+
303
+ limits[name] = value
304
+ end
305
+ limits
306
+ end
307
+
274
308
  def find_metadata_start
275
309
  metadata_max_size = [@size, METADATA_MAX_SIZE].min
276
310