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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +3 -3
- data/lib/maxmind/db/decoder.rb +198 -63
- data/lib/maxmind/db/file_reader.rb +5 -0
- data/lib/maxmind/db/memory_reader.rb +18 -1
- data/lib/maxmind/db.rb +39 -5
- metadata +46 -79
- data/Gemfile +0 -5
- data/Gemfile.lock +0 -58
- data/README.dev.md +0 -4
- data/Rakefile +0 -15
- data/bin/mmdb-benchmark.rb +0 -64
- data/maxmind-db.gemspec +0 -28
- data/test/data/LICENSE +0 -4
- data/test/data/MaxMind-DB-spec.md +0 -574
- data/test/data/README.md +0 -4
- data/test/data/bad-data/README.md +0 -7
- data/test/data/bad-data/libmaxminddb/libmaxminddb-offset-integer-overflow.mmdb +0 -0
- data/test/data/bad-data/maxminddb-golang/cyclic-data-structure.mmdb +0 -0
- data/test/data/bad-data/maxminddb-golang/invalid-bytes-length.mmdb +0 -1
- data/test/data/bad-data/maxminddb-golang/invalid-data-record-offset.mmdb +0 -0
- data/test/data/bad-data/maxminddb-golang/invalid-map-key-length.mmdb +0 -0
- data/test/data/bad-data/maxminddb-golang/invalid-string-length.mmdb +0 -1
- data/test/data/bad-data/maxminddb-golang/metadata-is-an-uint128.mmdb +0 -1
- data/test/data/bad-data/maxminddb-golang/unexpected-bytes.mmdb +0 -0
- data/test/data/bad-data/maxminddb-python/bad-unicode-in-map-key.mmdb +0 -0
- data/test/data/perltidyrc +0 -12
- data/test/data/source-data/GeoIP2-Anonymous-IP-Test.json +0 -49
- data/test/data/source-data/GeoIP2-City-Test.json +0 -12885
- data/test/data/source-data/GeoIP2-Connection-Type-Test.json +0 -122
- data/test/data/source-data/GeoIP2-Country-Test.json +0 -15904
- data/test/data/source-data/GeoIP2-DensityIncome-Test.json +0 -14
- data/test/data/source-data/GeoIP2-Domain-Test.json +0 -452
- data/test/data/source-data/GeoIP2-Enterprise-Test.json +0 -745
- data/test/data/source-data/GeoIP2-ISP-Test.json +0 -12605
- data/test/data/source-data/GeoIP2-Precision-Enterprise-Test.json +0 -2205
- data/test/data/source-data/GeoIP2-Static-IP-Score-Test.json +0 -2132
- data/test/data/source-data/GeoIP2-User-Count-Test.json +0 -2837
- data/test/data/source-data/GeoLite2-ASN-Test.json +0 -4120
- data/test/data/source-data/GeoLite2-City-Test.json +0 -12804
- data/test/data/source-data/GeoLite2-Country-Test.json +0 -11280
- data/test/data/source-data/README +0 -15
- data/test/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb +0 -0
- data/test/data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb +0 -0
- data/test/data/test-data/GeoIP2-City-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-Connection-Type-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-Country-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-DensityIncome-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-Domain-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-Enterprise-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-ISP-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-Precision-Enterprise-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-Static-IP-Score-Test.mmdb +0 -0
- data/test/data/test-data/GeoIP2-User-Count-Test.mmdb +0 -0
- data/test/data/test-data/GeoLite2-ASN-Test.mmdb +0 -0
- data/test/data/test-data/GeoLite2-City-Test.mmdb +0 -0
- data/test/data/test-data/GeoLite2-Country-Test.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-string-value-entries.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-broken-pointers-24.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-broken-search-tree-24.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-decoder.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-ipv4-28.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-ipv4-32.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-ipv6-24.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-ipv6-28.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-ipv6-32.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-mixed-24.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-mixed-28.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-mixed-32.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-nested.mmdb +0 -0
- data/test/data/test-data/MaxMind-DB-test-pointer-decoder.mmdb +0 -0
- data/test/data/test-data/README.md +0 -26
- data/test/data/test-data/maps-with-pointers.raw +0 -0
- data/test/data/test-data/write-test-data.pl +0 -695
- data/test/data/tidyall.ini +0 -5
- data/test/mmdb_util.rb +0 -26
- data/test/test_decoder.rb +0 -241
- data/test/test_reader.rb +0 -547
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19db0a99249703b86573eb824fe94f4eb4c807528699c7707b2b8ef7d22f89c9
|
|
4
|
+
data.tar.gz: efb878012b9565ffc0b7d9fde72b02685c85bbafe28819550ec184f88709640b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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://
|
|
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 -
|
|
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.
|
data/lib/maxmind/db/decoder.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
119
|
-
value, 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
|
-
|
|
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
|
-
|
|
136
|
-
pointer
|
|
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 =
|
|
140
|
-
pointer = buf.
|
|
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
|
-
|
|
144
|
-
pointer = buf.unpack1('N') + @pointer_base
|
|
240
|
+
pointer = @io.read(offset, 4).unpack1('N')
|
|
145
241
|
end
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
size = 29 + size_bytes.ord
|
|
357
|
+
size = 29 + @io.getbyte(offset)
|
|
223
358
|
return size, offset + 1
|
|
224
359
|
end
|
|
225
360
|
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
# @
|
|
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 [
|
|
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
|
|