Skip to content

Commit b4bb304

Browse files
oschwaldclaude
andcommitted
Update libmaxminddb 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. Move the libmaxminddb submodule to the main-branch commit that adds the decoder resource limits (maxmind/libmaxminddb#479), ahead of the 1.14.0 release. 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 7367b20 commit b4bb304

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 oversized variable-length integer is rejected before its bytes are
2424
copied.
25+
* The vendored libmaxminddb was updated to the commit that 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

@@ -642,3 +648,109 @@ class TestFDResourceLimits(BaseResourceLimitTest):
642648

643649

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

0 commit comments

Comments
 (0)