Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

## 1.5.0
Comment thread
oschwald marked this conversation as resolved.

* Fixed two denial-of-service issues in the decoder. A crafted database could
nest data-section pointers to shared targets so that decoding one record
cost exponential time and memory from a small file, or point many times at
one large string or bytes value so that a record with few values
materialized gigabytes. The decoder now bounds each record it decodes and
the metadata decoded when a database is opened. A database that exceeds a
limit raises `InvalidDatabaseError`. The limits are:
* 65,536 decoded values, as the MaxMind DB specification recommends.
* 512 levels of nesting, as the specification recommends. This also stops
pointer cycles.
* 2 MiB of string, bytes, and integer payload. The specification leaves this
limit to the reader. 2 MiB matches libmaxminddb.
* The decoder limits can be changed with the new `max_values`,
`max_payload_bytes`, and `max_depth` options to `MaxMind::DB.new`.
* Pointers that target other pointers are now rejected as invalid, as required
by the MaxMind DB specification.
* Lookups are faster. The decoder allocates fewer strings and dispatches on the
data type with a jump table. GeoLite City lookups in memory mode on CRuby
3.4 are about 18% faster than in 1.4.0.
* Unnecessary files were removed from the published .gem.

## 1.4.0 (2025-11-20)
Expand Down
44 changes: 39 additions & 5 deletions lib/maxmind/db.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,35 @@ class DB
# @param database [String] a path to a {MaxMind
# DB}[https://maxmind.github.io/MaxMind-DB/].
#
# @param options [Hash<Symbol, Symbol>] options controlling the behavior of
# @param options [Hash<Symbol, Object>] options controlling the behavior of
# the DB.
#
# @option options [Symbol] :mode Defines how to open the database. It may
# be one of MODE_AUTO, MODE_FILE, or MODE_MEMORY. If you don't provide
# one, DB uses MODE_AUTO. Refer to the definition of those constants for
# an explanation of their meaning.
#
# @raise [InvalidDatabaseError] if the database is corrupt or invalid.
# @option options [Integer] :max_values The maximum number of values a
# single record, or the metadata, may decode to. The default is 65,536.
# The largest records MaxMind produces decode to a few hundred values.
#
# @option options [Integer] :max_payload_bytes The maximum total size in
# bytes of the strings, bytes, and integers a single record, or the
# metadata, may decode. The default is 2 MiB. The largest records MaxMind
# produces hold about a kilobyte.
#
# @option options [Integer] :max_depth The maximum nesting depth of maps,
# arrays, and pointers in a single record, or the metadata. The default
# is 512.
#
# @raise [ArgumentError] if the mode is invalid.
# @raise [InvalidDatabaseError] if the database is corrupt or invalid. A
# database that exceeds any of the limits above raises this error from
# the lookup, or from this constructor if the metadata exceeds them.
#
# @raise [ArgumentError] if the mode or a limit is invalid.
def initialize(database, options = {})
options[:mode] = MODE_AUTO unless options.key?(:mode)
limits = decoder_limits(options)

case options[:mode]
when MODE_AUTO, MODE_FILE
Expand All @@ -101,11 +117,11 @@ def initialize(database, options = {})
@size = @io.size

metadata_start = find_metadata_start
metadata_decoder = Decoder.new(@io, metadata_start)
metadata_decoder = Decoder.new(@io, metadata_start, **limits)
metadata_map, = metadata_decoder.decode(metadata_start)
@metadata = Metadata.new(metadata_map)
@decoder = Decoder.new(@io, @metadata.search_tree_size +
DATA_SECTION_SEPARATOR_SIZE)
DATA_SECTION_SEPARATOR_SIZE, **limits)

# Store copies as instance variables to reduce method calls.
@ip_version = @metadata.ip_version
Expand Down Expand Up @@ -271,6 +287,24 @@ def resolve_data_pointer(pointer)
data
end

LIMIT_OPTIONS = %i[max_values max_payload_bytes max_depth].freeze
private_constant :LIMIT_OPTIONS

# Return the decoder limits given in +options+ as keyword arguments for
# Decoder.new. An absent option keeps the decoder's default.
def decoder_limits(options)
limits = {}
LIMIT_OPTIONS.each do |name|
next unless options.key?(name)

value = options[name]
raise ArgumentError, "#{name} must be a positive integer" unless value.is_a?(Integer) && value.positive?

limits[name] = value
end
limits
end

def find_metadata_start
metadata_max_size = [@size, METADATA_MAX_SIZE].min

Expand Down
Loading
Loading