Skip to content

Commit 287c569

Browse files
oschwaldclaude
andcommitted
Update libmaxminddb to 1.14.0 and test the extension rejects the DoS fixtures
The existing resource-limit tests force the pure Python modes, so they cover only the pure Python decoder. The C extension decodes through the vendored libmaxminddb, and nothing asserted that path rejects the DoS fixtures. Update the libmaxminddb submodule to 1.14.0, which adds the decoder resource limits (maxmind/libmaxminddb#479). Add extension-path checks that decode each DoS fixture through MODE_MMAP_EXT and assert an InvalidDatabaseError, and check that the amplified metadata fixture is rejected when the database is opened. The checks first probe a fixture one byte over the 2 MiB payload limit, which is small and safe to decode. The bundled library must reject it with the decoder-limit message. A system library selected with MAXMINDDB_USE_SYSTEM_LIBMAXMINDDB may predate the limits and decode it; the checks then skip rather than run the large DoS fixtures through a decoder that would exhaust memory. See GHSA-hj94-g986-h9r7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent db0b269 commit 287c569

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

HISTORY.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ History
2222
``InvalidDatabaseError``.
2323
* An integer that declares more bytes than its type allows is rejected
2424
before its bytes are read.
25+
* The vendored libmaxminddb was updated to 1.14.0, which adds the same
26+
limits to the C extension.
2527

2628
3.1.1 (2026-03-05)
2729
++++++++++++++++++

tests/decoder_test.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@
1313
MODE_FILE,
1414
MODE_MEMORY,
1515
MODE_MMAP,
16+
MODE_MMAP_EXT,
1617
open_database,
1718
)
1819
from maxminddb.decoder import Decoder
1920
from maxminddb.errors import InvalidDatabaseError
2021

22+
try:
23+
import maxminddb.extension as _extension
24+
except ImportError:
25+
_extension = None # type: ignore[assignment]
26+
2127
if TYPE_CHECKING:
2228
from collections.abc import Iterator
2329

@@ -651,3 +657,109 @@ class TestFDResourceLimits(BaseResourceLimitTest):
651657

652658

653659
del BaseResourceLimitTest
660+
661+
662+
def _has_extension() -> bool:
663+
return _extension is not None and hasattr(_extension, "Reader")
664+
665+
666+
# The patched libmaxminddb reports its decoder resource limits through this
667+
# text (MMDB_DECODER_LIMIT_ERROR). A libmaxminddb without the fix decodes the
668+
# DoS fixtures instead, so the tests below skip rather than run the extension's
669+
# decoder out of memory.
670+
_EXTENSION_LIMIT_MESSAGE = "exceeds the configured resource limits"
671+
672+
673+
@unittest.skipUnless(_has_extension(), "C extension not available")
674+
class TestExtensionResourceLimits(unittest.TestCase):
675+
"""DoS-fixture checks for the C extension's libmaxminddb decoder.
676+
677+
The extension decodes through libmaxminddb, so these limits live in that
678+
library, not in the pure-Python decoder that the BaseResourceLimitTest
679+
subclasses cover. A system libmaxminddb without the limits skips the
680+
checks; see setUp.
681+
"""
682+
683+
@staticmethod
684+
def _lookup(filename: str, ip: str = "0.0.0.1") -> object:
685+
# MODE_MMAP_EXT forces the C extension. Each DoS fixture resolves any
686+
# IPv4 address to its single crafted record.
687+
with open_database(
688+
f"{_TEST_DATA_DIR}/{filename}",
689+
mode=MODE_MMAP_EXT,
690+
) as reader:
691+
return reader.get(ip)
692+
693+
def setUp(self) -> None:
694+
# Probe with a fixture one byte over the 2 MiB payload limit, which is
695+
# small and safe to decode even without the limits. The bundled
696+
# libmaxminddb has them, so it must reject the probe with the
697+
# decoder-limit message; anything else is a failure. A system library
698+
# selected with MAXMINDDB_USE_SYSTEM_LIBMAXMINDDB may predate the
699+
# limits and decode the probe. Skip then, rather than run the large
700+
# DoS fixtures through a decoder that would exhaust memory.
701+
try:
702+
self._lookup("MaxMind-DB-test-decoder-payload-limit-over.mmdb")
703+
except InvalidDatabaseError as exc:
704+
if _EXTENSION_LIMIT_MESSAGE in str(exc):
705+
return
706+
raise
707+
if not os.environ.get("MAXMINDDB_USE_SYSTEM_LIBMAXMINDDB"):
708+
self.fail(
709+
"the bundled libmaxminddb decoded a record over the payload limit"
710+
)
711+
self.skipTest(
712+
"system libmaxminddb predates the decoder resource limits "
713+
"(needs the release that adds MMDB_DECODER_LIMIT_ERROR)",
714+
)
715+
716+
def test_pointer_fan_out_fixture_is_rejected(self) -> None:
717+
# A full database whose record nests arrays of pointers to the level
718+
# below, the classic 2**depth fan-out.
719+
with (
720+
_bounded(),
721+
self.assertRaisesRegex(InvalidDatabaseError, _EXTENSION_LIMIT_MESSAGE),
722+
):
723+
self._lookup("MaxMind-DB-test-pointer-decoder-dos.mmdb")
724+
725+
def test_pointer_fan_out_ipv6_fixture_is_rejected(self) -> None:
726+
# The IPv6 fan-out database, so the extension's IPv6 tree path is
727+
# covered too.
728+
with (
729+
_bounded(),
730+
self.assertRaisesRegex(InvalidDatabaseError, _EXTENSION_LIMIT_MESSAGE),
731+
):
732+
self._lookup("MaxMind-DB-test-pointer-decoder-dos-ipv6.mmdb", "2001:db8::1")
733+
734+
def test_metadata_payload_limit_is_enforced_on_open(self) -> None:
735+
# libmaxminddb rejects the amplified metadata in MMDB_open, which the
736+
# extension reports as a generic open failure.
737+
with _bounded(), self.assertRaisesRegex(InvalidDatabaseError, "Error opening"):
738+
open_database(
739+
f"{_TEST_DATA_DIR}/MaxMind-DB-test-metadata-payload-limit.mmdb",
740+
mode=MODE_MMAP_EXT,
741+
)
742+
743+
def test_payload_amplification_is_rejected(self) -> None:
744+
# An array of 8,192 pointers to one 65,535-byte value.
745+
with (
746+
_bounded(),
747+
self.assertRaisesRegex(InvalidDatabaseError, _EXTENSION_LIMIT_MESSAGE),
748+
):
749+
self._lookup("MaxMind-DB-test-payload-amplification-dos.mmdb")
750+
751+
def test_payload_amplification_string_is_rejected(self) -> None:
752+
# The UTF-8 string variant, so the string decode path is exercised.
753+
with (
754+
_bounded(),
755+
self.assertRaisesRegex(InvalidDatabaseError, _EXTENSION_LIMIT_MESSAGE),
756+
):
757+
self._lookup("MaxMind-DB-test-payload-amplification-dos-string.mmdb")
758+
759+
def test_payload_amplification_worst_case_is_rejected(self) -> None:
760+
# 65,535 pointers to one 65,535-byte value, exactly the value limit.
761+
with (
762+
_bounded(),
763+
self.assertRaisesRegex(InvalidDatabaseError, _EXTENSION_LIMIT_MESSAGE),
764+
):
765+
self._lookup("MaxMind-DB-test-payload-amplification-dos-worst-case.mmdb")

0 commit comments

Comments
 (0)