1818 from maxminddb .file import FileBuffer
1919 from maxminddb .types import Record
2020
21- DecoderFunc = Callable [["Decoder" , int , int ], tuple [Record , int ]]
21+ DecoderFunc = Callable [["Decoder" , int , int , list [int ]], tuple [Record , int ]]
22+
23+
24+ # Per-lookup limit on the number of values decoded, recommended by the MaxMind
25+ # DB specification. It stops a pointer fan-out, where nested pointers to shared
26+ # targets would otherwise cost 2**depth decode operations. The largest real
27+ # records decode a few hundred values, so the limit leaves a wide margin.
28+ # Pointer cycles and over-deep data are caught separately by Python's own
29+ # recursion limit (see ``decode``).
30+ _MAX_VALUES = 1 << 16
31+ _TOO_MANY_VALUES = (
32+ "The MaxMind DB file's data section exceeds the maximum number of values"
33+ )
2234
2335
2436class Decoder :
@@ -42,35 +54,68 @@ def __init__(
4254 self ._buffer = database_buffer
4355 self ._pointer_base = pointer_base
4456
45- def _decode_array (self , size : int , offset : int ) -> tuple [list [Record ], int ]:
57+ def _decode_array (
58+ self ,
59+ size : int ,
60+ offset : int ,
61+ budget : list [int ],
62+ ) -> tuple [list [Record ], int ]:
63+ budget [0 ] -= size
64+ if budget [0 ] < 0 :
65+ raise InvalidDatabaseError (_TOO_MANY_VALUES )
4666 array = []
4767 for _ in range (size ):
48- (value , offset ) = self .decode (offset )
68+ (value , offset ) = self ._decode (offset , budget )
4969 array .append (value )
5070 return array , offset
5171
52- def _decode_boolean (self , size : int , offset : int ) -> tuple [bool , int ]:
72+ def _decode_boolean (
73+ self ,
74+ size : int ,
75+ offset : int ,
76+ _budget : list [int ],
77+ ) -> tuple [bool , int ]:
5378 return size != 0 , offset
5479
55- def _decode_bytes (self , size : int , offset : int ) -> tuple [bytes , int ]:
80+ def _decode_bytes (
81+ self ,
82+ size : int ,
83+ offset : int ,
84+ _budget : list [int ],
85+ ) -> tuple [bytes , int ]:
5686 new_offset = offset + size
5787 return self ._buffer [offset :new_offset ], new_offset
5888
59- def _decode_double (self , size : int , offset : int ) -> tuple [float , int ]:
89+ def _decode_double (
90+ self ,
91+ size : int ,
92+ offset : int ,
93+ _budget : list [int ],
94+ ) -> tuple [float , int ]:
6095 self ._verify_size (size , 8 )
6196 new_offset = offset + size
6297 packed_bytes = self ._buffer [offset :new_offset ]
6398 (value ,) = struct .unpack (b"!d" , packed_bytes )
6499 return value , new_offset
65100
66- def _decode_float (self , size : int , offset : int ) -> tuple [float , int ]:
101+ def _decode_float (
102+ self ,
103+ size : int ,
104+ offset : int ,
105+ _budget : list [int ],
106+ ) -> tuple [float , int ]:
67107 self ._verify_size (size , 4 )
68108 new_offset = offset + size
69109 packed_bytes = self ._buffer [offset :new_offset ]
70110 (value ,) = struct .unpack (b"!f" , packed_bytes )
71111 return value , new_offset
72112
73- def _decode_int32 (self , size : int , offset : int ) -> tuple [int , int ]:
113+ def _decode_int32 (
114+ self ,
115+ size : int ,
116+ offset : int ,
117+ _budget : list [int ],
118+ ) -> tuple [int , int ]:
74119 if size == 0 :
75120 return 0 , offset
76121 new_offset = offset + size
@@ -81,15 +126,29 @@ def _decode_int32(self, size: int, offset: int) -> tuple[int, int]:
81126 (value ,) = struct .unpack (b"!i" , packed_bytes )
82127 return value , new_offset
83128
84- def _decode_map (self , size : int , offset : int ) -> tuple [dict [str , Record ], int ]:
129+ def _decode_map (
130+ self ,
131+ size : int ,
132+ offset : int ,
133+ budget : list [int ],
134+ ) -> tuple [dict [str , Record ], int ]:
135+ # A map entry decodes a key and a value, so it costs two values.
136+ budget [0 ] -= size * 2
137+ if budget [0 ] < 0 :
138+ raise InvalidDatabaseError (_TOO_MANY_VALUES )
85139 container : dict [str , Record ] = {}
86140 for _ in range (size ):
87- (key , offset ) = self .decode (offset )
88- (value , offset ) = self .decode (offset )
141+ (key , offset ) = self ._decode (offset , budget )
142+ (value , offset ) = self ._decode (offset , budget )
89143 container [cast ("str" , key )] = value
90144 return container , offset
91145
92- def _decode_pointer (self , size : int , offset : int ) -> tuple [Record , int ]:
146+ def _decode_pointer (
147+ self ,
148+ size : int ,
149+ offset : int ,
150+ budget : list [int ],
151+ ) -> tuple [Record , int ]:
93152 pointer_size = (size >> 3 ) + 1
94153
95154 buf = self ._buffer [offset : offset + pointer_size ]
@@ -109,15 +168,26 @@ def _decode_pointer(self, size: int, offset: int) -> tuple[Record, int]:
109168
110169 if self ._pointer_test :
111170 return pointer , new_offset
112- (value , _ ) = self .decode (pointer )
171+
172+ (value , _ ) = self ._decode (pointer , budget )
113173 return value , new_offset
114174
115- def _decode_uint (self , size : int , offset : int ) -> tuple [int , int ]:
175+ def _decode_uint (
176+ self ,
177+ size : int ,
178+ offset : int ,
179+ _budget : list [int ],
180+ ) -> tuple [int , int ]:
116181 new_offset = offset + size
117182 uint_bytes = self ._buffer [offset :new_offset ]
118183 return int .from_bytes (uint_bytes , "big" ), new_offset
119184
120- def _decode_utf8_string (self , size : int , offset : int ) -> tuple [str , int ]:
185+ def _decode_utf8_string (
186+ self ,
187+ size : int ,
188+ offset : int ,
189+ _budget : list [int ],
190+ ) -> tuple [str , int ]:
121191 new_offset = offset + size
122192 return self ._buffer [offset :new_offset ].decode ("utf-8" ), new_offset
123193
@@ -144,6 +214,19 @@ def decode(self, offset: int) -> tuple[Record, int]:
144214 offset: the location of the data structure to decode
145215
146216 """
217+ # Bound the work per lookup so a crafted database cannot exhaust CPU or
218+ # memory. ``budget`` is a single-element list so the running count is
219+ # shared across the recursion. It is call-local, which keeps the
220+ # decoder safe for concurrent reads. There is no separate depth limit: a
221+ # pointer cycle or over-deep data exhausts Python's own recursion limit,
222+ # which is converted into an InvalidDatabaseError.
223+ try :
224+ return self ._decode (offset , [_MAX_VALUES ])
225+ except RecursionError as ex :
226+ msg = "The MaxMind DB file's data section exceeds the maximum depth"
227+ raise InvalidDatabaseError (msg ) from ex
228+
229+ def _decode (self , offset : int , budget : list [int ]) -> tuple [Record , int ]:
147230 new_offset = offset + 1
148231 ctrl_byte = self ._buffer [offset ]
149232 type_num = ctrl_byte >> 5
@@ -160,7 +243,7 @@ def decode(self, offset: int) -> tuple[Record, int]:
160243 ) from ex
161244
162245 (size , new_offset ) = self ._size_from_ctrl_byte (ctrl_byte , new_offset , type_num )
163- return decoder (self , size , new_offset )
246+ return decoder (self , size , new_offset , budget )
164247
165248 def _read_extended (self , offset : int ) -> tuple [int , int ]:
166249 next_byte = self ._buffer [offset ]
0 commit comments