Skip to content
Open
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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CHANGELOG
=========

4.1.1
4.2.0
------------------

* Fixed decoding of data pointers with offsets of 2 GiB or greater. The
Expand All @@ -10,6 +10,19 @@ CHANGELOG
with an `IllegalArgumentException`. Every record past the 2 GiB
boundary was unreachable in databases larger than 2 GiB, which have
been supported since 4.0.0.
* Fixed a denial-of-service issue 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. The decoder now limits the
number of values it decodes for a single record and rejects a database that
exceeds it, along with pointer cycles and over-deep data, with an
`InvalidDatabaseException`. It also rejects a map or array whose declared size
is larger than the remaining data before allocating for it, so a crafted size
cannot force a large list or map allocation from a small file. A related shape
points many pointers at one large string or bytes value; the decoder now
bounds the total string and bytes payload it materializes for a single record,
charging each value as it is decoded, so re-decoding a shared target cannot
amplify a small file into gigabytes. This matches the reader resource limits
now recommended by the MaxMind DB specification. See GHSA-hj94-g986-h9r7.

4.1.0 (2026-05-12)
------------------
Expand Down
184 changes: 150 additions & 34 deletions src/main/java/com/maxmind/db/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.lang.reflect.ParameterizedType;
import java.math.BigInteger;
import java.net.InetAddress;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
Expand All @@ -35,6 +34,23 @@ class Decoder {

private final NodeCache cache;

// Per-lookup decode limits recommended by the MaxMind DB specification. The
// depth limit stops pointer cycles and over-deep data before the stack
// overflows. The value limit stops a pointer fan-out, where nested pointers
// to shared targets would otherwise cost 2**depth decode operations. The
// payload limit stops a payload amplification, where many pointers to one
// large string or bytes value would otherwise materialize N times its size:
// each string or bytes value is charged its length every time it is decoded,
// so re-decoding a shared target recharges it. A Decoder serves a single
// lookup on a single thread, so these need no synchronization. The largest
// real records decode a few hundred values and a few kilobytes of payload.
private static final int MAX_DEPTH = 512;
private static final int MAX_VALUES = 1 << 16;
private static final long MAX_PAYLOAD_BYTES = 1 << 21;
private int depth;
private int valuesRemaining = MAX_VALUES;
private long payloadRemaining = MAX_PAYLOAD_BYTES;

private final long pointerBase;

private final CharsetDecoder utfDecoder = UTF_8.newDecoder();
Expand Down Expand Up @@ -104,6 +120,9 @@ <T> T decode(long offset, Class<T> cls) throws IOException {
+ "pointer larger than the database.");
}

this.valuesRemaining = MAX_VALUES;
this.payloadRemaining = MAX_PAYLOAD_BYTES;
this.depth = 0;
this.buffer.position(offset);
return cls.cast(decode(cls, null).value());
}
Expand All @@ -122,6 +141,15 @@ private <T> DecodedValue decode(CacheKey<T> key) throws IOException {
}

private <T> DecodedValue decode(Class<T> cls, java.lang.reflect.Type genericType)
throws IOException {
if (--this.valuesRemaining < 0) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum number of values");
}
return decodeValue(cls, genericType);
}

private <T> DecodedValue decodeValue(Class<T> cls, java.lang.reflect.Type genericType)
throws IOException {
var ctrlByte = 0xFF & this.buffer.get();

Expand Down Expand Up @@ -168,6 +196,16 @@ private <T> DecodedValue decode(Class<T> cls, java.lang.reflect.Type genericType

DecodedValue decodePointer(long pointer, Class<?> cls, java.lang.reflect.Type genericType)
throws IOException {
// A pointer to another pointer is illegal per the specification. It also
// lets a pointer cycle recurse without ever entering a container, which
// the depth limit would not catch, so reject it here. Container cycles
// and over-deep data are bounded by the depth limit in decodeByType.
if (pointer < buffer.capacity()
&& Type.fromControlByte(0xFF & buffer.get(pointer)) == Type.POINTER) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section contains a pointer to a pointer");
}
Comment thread
oschwald marked this conversation as resolved.

var position = buffer.position();

var key = new CacheKey<>(pointer, cls, genericType);
Expand Down Expand Up @@ -216,15 +254,62 @@ private static boolean isSimpleType(Class<?> cls) {
|| cls.equals(BigInteger.class);
}

// A container cannot hold more entries than there are bytes left to encode
// them: every key, value, and element occupies at least one byte. Reject an
// impossible declared size before it is used as an allocation hint, so a
// tiny crafted database cannot force a huge list or map preallocation and
// exhaust memory. valueCount is the number of encoded values the container
// declares (an array of N declares N, a map of N declares 2N).
private void checkContainerSize(long valueCount) throws InvalidDatabaseException {
// A container cannot decode more values than the per-lookup budget
// allows, so reject an oversized declaration before allocating for it
// rather than after the per-value limit stops the decode.
if (valueCount > this.valuesRemaining) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum number of values");
}
if (valueCount > this.buffer.capacity() - this.buffer.position()) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section contains bad data: "
+ "a container declares more entries than the data section can hold");
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
oschwald marked this conversation as resolved.

// Charge a string or bytes payload against the per-lookup budget before it
// is materialized. A payload amplification points many pointers at one large
// value; because the budget is charged every time the value is decoded, and
// a shared pointer target is re-decoded per referencing pointer, N pointers
// to an S-byte value are charged N*S and rejected once the total exceeds the
// limit. Charging before allocation also bounds an oversized variable-length
// integer, whose declared size the decoder would otherwise copy before
// range-checking. The comparison is against the remaining budget so it
// cannot overflow. The limit is inclusive: a total exactly at the limit is
// allowed.
private void chargePayload(long length) throws InvalidDatabaseException {
if (length > this.payloadRemaining) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum payload size");
}
this.payloadRemaining -= length;
}

private <T> Object decodeByType(
Type type,
int size,
Class<T> cls,
java.lang.reflect.Type genericType
) throws IOException {
switch (type) {
case MAP:
return this.decodeMap(size, cls, genericType);
case MAP: {
if (++this.depth > MAX_DEPTH) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum depth");
}
this.checkContainerSize((long) size * 2);
var map = this.decodeMap(size, cls, genericType);
Comment thread
oschwald marked this conversation as resolved.
this.depth--;
return map;
}
case ARRAY:
Class<?> elementClass = Object.class;
if (genericType instanceof ParameterizedType ptype) {
Expand All @@ -233,7 +318,14 @@ private <T> Object decodeByType(
elementClass = (Class<?>) actualTypes[0];
}
}
return this.decodeArray(size, cls, elementClass);
if (++this.depth > MAX_DEPTH) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum depth");
}
this.checkContainerSize(size);
var array = this.decodeArray(size, cls, elementClass);
this.depth--;
return array;
case BOOLEAN:
Boolean bool = Decoder.decodeBoolean(size);
return convertValue(bool, cls);
Expand Down Expand Up @@ -377,7 +469,8 @@ private static Object coerceFromBigInteger(BigInteger value, Class<?> target) {
return value;
}

private String decodeString(long size) throws CharacterCodingException {
private String decodeString(long size) throws IOException {
this.chargePayload(size);
var oldLimit = buffer.limit();
buffer.limit(buffer.position() + size);
var s = buffer.decode(utfDecoder);
Expand Down Expand Up @@ -425,7 +518,7 @@ static int decodeInteger(Buffer buffer, int base, int size) {
return integer;
}

private BigInteger decodeBigInteger(int size) {
private BigInteger decodeBigInteger(int size) throws InvalidDatabaseException {
var bytes = this.getByteArray(size);
return new BigInteger(1, bytes);
}
Expand Down Expand Up @@ -1104,35 +1197,57 @@ private static Object parseDefault(String value, Class<?> target) {

private long nextValueOffset(long offset, int numberToSkip)
throws InvalidDatabaseException {
if (numberToSkip == 0) {
return offset;
}

var ctrlData = this.getCtrlData(offset);
var ctrlByte = ctrlData.ctrlByte();
var size = ctrlData.size();
offset = ctrlData.offset();
// Iterate over siblings so a large flat unknown value cannot exhaust
// the Java stack. Recursion is only used to track structural nesting,
// which is bounded by the same limit as normal decoding.
for (var i = 0; i < numberToSkip; i++) {
if (--this.valuesRemaining < 0) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum number of values");
}

var type = ctrlData.type();
switch (type) {
case POINTER:
var pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
offset += pointerSize;
break;
case MAP:
numberToSkip += 2 * size;
break;
case ARRAY:
numberToSkip += size;
break;
case BOOLEAN:
break;
default:
offset += size;
break;
var ctrlData = this.getCtrlData(offset);
var ctrlByte = ctrlData.ctrlByte();
var size = ctrlData.size();
offset = ctrlData.offset();

switch (ctrlData.type()) {
case POINTER:
var pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
offset += pointerSize;
break;
case MAP:
this.checkContainerSize((long) size * 2);
if (++this.depth > MAX_DEPTH) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum depth");
}
try {
offset = this.nextValueOffset(offset, 2 * size);
} finally {
this.depth--;
}
break;
case ARRAY:
this.checkContainerSize(size);
if (++this.depth > MAX_DEPTH) {
throw new InvalidDatabaseException(
"The MaxMind DB file's data section exceeds the maximum depth");
}
try {
offset = this.nextValueOffset(offset, size);
} finally {
this.depth--;
}
break;
case BOOLEAN:
break;
default:
offset += size;
break;
}
}

return nextValueOffset(offset, numberToSkip - 1);
return offset;
}

private CtrlData getCtrlData(long offset)
Expand Down Expand Up @@ -1179,7 +1294,8 @@ private CtrlData getCtrlData(long offset)
return new CtrlData(type, ctrlByte, offset, size);
}

private byte[] getByteArray(int length) {
private byte[] getByteArray(int length) throws InvalidDatabaseException {
this.chargePayload(length);
return Decoder.getByteArray(this.buffer, length);
}

Expand Down
Loading
Loading