Skip to content

Commit cf2ee83

Browse files
authored
Merge pull request #442 from maxmind/greg/stf-1488
Bound decoder work to prevent a pointer fan-out DoS (STF-1571)
2 parents dd71a2a + a326219 commit cf2ee83

14 files changed

Lines changed: 1641 additions & 212 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
CHANGELOG
22
=========
33

4-
4.1.1
4+
4.2.0
55
------------------
66

7-
* Fixed decoding of data pointers with offsets of 2 GiB or greater. The
8-
pointer payload was decoded into an `int`, so such offsets were
9-
sign-extended to a negative value and rejected by `Buffer.position()`
10-
with an `IllegalArgumentException`. Every record past the 2 GiB
11-
boundary was unreachable in databases larger than 2 GiB, which have
12-
been supported since 4.0.0.
7+
* Fixed decoding of data pointers with offsets of 2 GiB or greater. Records
8+
beyond that boundary could previously fail with `IllegalArgumentException`.
9+
* Fixed skipping unknown four-byte pointers during typed decoding. Skipped
10+
values that extend past the database are now rejected.
11+
* Fixed UTF-8 decoding across buffer chunks. Malformed decoded strings and
12+
truncated values now throw `InvalidDatabaseException`.
13+
* Added decoder limits to prevent excessive CPU and memory use from crafted
14+
databases: 65,536 decoded or skipped values, 128 nested containers, and 2 MiB
15+
of encoded string and bytes payload per operation. Exceeding a limit throws
16+
`InvalidDatabaseException`. See [UPGRADING.md](UPGRADING.md) for accounting,
17+
decoded-value validation, and collection capacity-hint changes.
18+
* Improved decoder performance and reduced per-lookup allocation, including
19+
UTF-8 string decoding.
1320

1421
4.1.0 (2026-05-12)
1522
------------------

‎UPGRADING.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
# Upgrading to 4.2.0
2+
3+
## Decoder Resource Limits
4+
5+
Version 4.2.0 limits the work and memory used by one record or metadata decode.
6+
The decoder rejects an operation that exceeds any of these limits:
7+
8+
- 65,536 decoded or skipped values under the Java reader's work accounting
9+
- 128 nested maps or arrays
10+
- 2 MiB of encoded string and bytes payload materialized by the decoder
11+
12+
A decoded pointer costs one value in addition to its target's logical costs.
13+
Cached targets retain their value count, container depth, and payload bytes, so
14+
cache state does not change whether a decode exceeds a limit. Skipped pointers
15+
count as one value and their targets remain unvisited. Skipped fields receive structural bounds and
16+
resource checks, but their contents are not fully validated.
17+
18+
These limits leave a wide margin above MaxMind-produced records. A custom
19+
database containing an unusually large record that decoded in an earlier
20+
release may now throw `InvalidDatabaseException`. The limits are not
21+
configurable in this release.
22+
23+
Initial collection capacity hints are capped at 128. Built-in collections grow
24+
as needed. Custom `List` and `Map` types constructed through an `int` constructor
25+
receive this capped hint instead of the full declared size.
26+
27+
When following a pointer, the decoder rejects targets that are themselves
28+
pointers. Decoded integers wider than their format type permits are also rejected.
29+
130
# Upgrading to 4.0.0
231

332
This guide covers the breaking changes introduced in version 4.0.0 and how to

‎src/main/java/com/maxmind/db/Buffer.java‎

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package com.maxmind.db;
22

3-
import java.nio.charset.CharacterCodingException;
4-
import java.nio.charset.CharsetDecoder;
5-
63
/**
74
* A generic buffer abstraction that supports sequential and random access
85
* to binary data. Implementations may be backed by a single {@link
@@ -96,13 +93,4 @@ sealed interface Buffer permits SingleBuffer, MultiBuffer {
9693
* @return a duplicate buffer
9794
*/
9895
Buffer duplicate();
99-
100-
/**
101-
* Decodes the buffer's content into a string using the given decoder.
102-
*
103-
* @param decoder the charset decoder
104-
* @return the decoded string
105-
* @throws CharacterCodingException if decoding fails
106-
*/
107-
String decode(CharsetDecoder decoder) throws CharacterCodingException;
10896
}
Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
11
package com.maxmind.db;
22

33
/**
4-
* {@code DecodedValue} is a wrapper for the decoded value.
4+
* An opaque decoded value and its resource costs, produced by {@link NodeCache.Loader}.
5+
* Caches retain this instance unchanged for its original key. See {@link NodeCache}.
56
*/
67
public final class DecodedValue {
8+
private static final int PAYLOAD_SHIFT = 8;
9+
private static final int VALUES_SHIFT = 30;
10+
private static final long PAYLOAD_MASK = (1L << 22) - 1;
11+
12+
// Final fields preserve their initialized values when a cache publishes this object.
713
final Object value;
14+
private final long costs;
815

9-
DecodedValue(Object value) {
16+
DecodedValue(Object value, int values, long payloadBytes, int depth) {
1017
this.value = value;
18+
this.costs = ((long) values << VALUES_SHIFT)
19+
| (payloadBytes << PAYLOAD_SHIFT)
20+
| depth;
1121
}
1222

1323
Object value() {
1424
return value;
1525
}
26+
27+
static int values(long costs) {
28+
return (int) (costs >>> VALUES_SHIFT);
29+
}
30+
31+
static long payloadBytes(long costs) {
32+
return (costs >>> PAYLOAD_SHIFT) & PAYLOAD_MASK;
33+
}
34+
35+
static int depth(long costs) {
36+
return (int) (costs & 0xFF);
37+
}
38+
39+
long costs() {
40+
return this.costs;
41+
}
1642
}

0 commit comments

Comments
 (0)