Skip to content

Commit 3eda67f

Browse files
authored
Merge pull request #416 from maxmind/greg/eng-4239
Harden libmaxminddb against crafted/malformed databases
2 parents 095ab13 + 5b71ac4 commit 3eda67f

19 files changed

Lines changed: 738 additions & 32 deletions

Changes.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,59 @@
11
## 1.13.0
22

3+
* `MMDB_get_entry_data_list()` now validates that the claimed array/map
4+
size is plausible given the remaining bytes in the data section. A
5+
crafted database could previously claim millions of array elements
6+
while only having a few bytes of data, causing disproportionate memory
7+
allocation (memory amplification DoS).
8+
* On Windows, `GetFileSize()` was replaced with `GetFileSizeEx()` to
9+
correctly handle files larger than 4GB. The previous code passed
10+
`NULL` for the high DWORD, discarding the upper 32 bits of the file
11+
size.
12+
* Fixed integer overflow in `MMDB_read_node()` and `find_ipv4_start_node()`
13+
pointer arithmetic. The `node_number * record_length` multiplication
14+
was performed in `uint32_t`, which could overflow for very large
15+
databases. Now cast to `uint64_t` before multiplying, matching the
16+
pattern already used in `find_address_in_search_tree()`.
17+
* Fixed printf format specifier mismatches in `mmdblookup`'s metadata
18+
dump. `%i` was used for unsigned types and `%llu` for `uint64_t`,
19+
which is technically undefined behavior. Now uses the portable
20+
`PRIu32`, `PRIu16`, and `PRIu64` macros from `<inttypes.h>`.
21+
* Fixed an integer overflow in the search tree bounds check in
22+
`find_address_in_search_tree()`. The addition of `node_count` and
23+
`data_section_size` was performed in `uint32_t` arithmetic, which
24+
could wrap on very large databases, causing valid lookups to be
25+
incorrectly rejected as corrupt.
26+
* Fixed a NULL pointer dereference in `mmdblookup` when displaying
27+
metadata for a database with an out-of-range `build_epoch`. The
28+
`gmtime()` return value is now checked before passing to `strftime()`.
29+
* `MMDB_close()` now NULLs the `file_content`, `data_section`, and
30+
`metadata_section` pointers and zeroes `file_size`, `data_section_size`,
31+
and `metadata_section_size` after unmapping. Previously, calling
32+
`MMDB_close()` twice on the same struct (or calling it after a failed
33+
`MMDB_open()` that succeeded at mapping) would double-munmap the file
34+
content, which is undefined behavior.
35+
* Fixed a stack buffer overflow in `print_indentation()` when
36+
`MMDB_dump_entry_data_list()` was called with a negative `indent`
37+
value. The negative integer was cast to `size_t`, producing a massive
38+
value passed to `memset()`. Negative indent values are now clamped
39+
to 0.
40+
* `MMDB_lookup_string()` now sets `*mmdb_error` to `MMDB_SUCCESS` when
41+
`getaddrinfo` fails (non-zero `*gai_error`). Previously, `*mmdb_error`
42+
was left uninitialized in this case, which could cause callers to read
43+
an indeterminate value.
44+
* Fixed an off-by-one in `mmdblookup` on Windows where `alloca` allocated
45+
one byte too few for the program name buffer, causing `_splitpath` to
46+
write one byte past the end when appending the null terminator.
47+
* Added a recursion depth limit to `skip_map_or_array()`, matching the
48+
existing `MAXIMUM_DATA_STRUCTURE_DEPTH` (512) limit already used by
49+
`get_entry_data_list()`. A crafted MMDB file with deeply nested maps
50+
or arrays could previously cause a stack overflow via unbounded
51+
recursion in the `MMDB_aget_value` / `MMDB_get_value` code path.
52+
* Fixed an off-by-one error in `MMDB_read_node()` that allowed reading one
53+
node past the end of the search tree when called with
54+
`node_number == node_count`. This caused the function to read from the
55+
data section separator and return an invalid record with an underflowed
56+
data offset. The check now correctly rejects `node_number >= node_count`.
357
* The handling of float and double types was rewritten to fix compiler errors
458
and to eliminate the use of volatile.
559
* Improved endian preprocessor check if `MMDB_LITTLE_ENDIAN` is not set.

bin/mmdblookup.c

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "maxminddb.h"
99
#include <errno.h>
1010
#include <getopt.h>
11+
#include <inttypes.h>
1112
#ifndef _WIN32
1213
#include <pthread.h>
1314
#endif
@@ -229,7 +230,7 @@ static const char **get_options(int argc,
229230
static int version = 0;
230231

231232
#ifdef _WIN32
232-
char *program = alloca(strlen(argv[0]));
233+
char *program = alloca(strlen(argv[0]) + 1);
233234
_splitpath(argv[0], NULL, NULL, program, NULL);
234235
_splitpath(argv[0], NULL, NULL, NULL, program + strlen(program));
235236
#else
@@ -346,17 +347,22 @@ static MMDB_s open_or_die(const char *fname) {
346347
static void dump_meta(MMDB_s *mmdb) {
347348
const char *meta_dump = "\n"
348349
" Database metadata\n"
349-
" Node count: %i\n"
350-
" Record size: %i bits\n"
351-
" IP version: IPv%i\n"
352-
" Binary format: %i.%i\n"
353-
" Build epoch: %llu (%s)\n"
350+
" Node count: %" PRIu32 "\n"
351+
" Record size: %" PRIu16 " bits\n"
352+
" IP version: IPv%" PRIu16 "\n"
353+
" Binary format: %" PRIu16 ".%" PRIu16 "\n"
354+
" Build epoch: %" PRIu64 " (%s)\n"
354355
" Type: %s\n"
355356
" Languages: ";
356357

357358
char date[40];
358359
const time_t epoch = (const time_t)mmdb->metadata.build_epoch;
359-
strftime(date, 40, "%F %T UTC", gmtime(&epoch));
360+
struct tm *tm = gmtime(&epoch);
361+
if (tm != NULL) {
362+
strftime(date, sizeof(date), "%F %T UTC", tm);
363+
} else {
364+
snprintf(date, sizeof(date), "out of range");
365+
}
360366

361367
fprintf(stdout,
362368
meta_dump,

doc/libmaxminddb.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,10 @@ This function always returns an `MMDB_lookup_result_s` structure, but you
489489
should also check the `gai_error` and `mmdb_error` parameters. If either of
490490
these indicates an error then the returned structure is meaningless.
491491

492+
When `*gai_error` is non-zero (i.e., `getaddrinfo()` failed), `*mmdb_error`
493+
is set to `MMDB_SUCCESS` because no database error occurred. You should always
494+
check `*gai_error` first.
495+
492496
If no error occurred you still need to make sure that the `found_entry` member
493497
in the returned result is true. If it's not, this means that the IP address
494498
does not have an entry in the database.

src/maxminddb.c

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ static int lookup_path_in_map(const char *path_elem,
178178
const MMDB_s *const mmdb,
179179
MMDB_entry_data_s *entry_data);
180180
static int skip_map_or_array(const MMDB_s *const mmdb,
181-
MMDB_entry_data_s *entry_data);
181+
MMDB_entry_data_s *entry_data,
182+
int depth);
182183
static int decode_one_follow(const MMDB_s *const mmdb,
183184
uint32_t offset,
184185
MMDB_entry_data_s *entry_data);
@@ -366,7 +367,7 @@ static LPWSTR utf8_to_utf16(const char *utf8_str) {
366367
}
367368

368369
static int map_file(MMDB_s *const mmdb) {
369-
DWORD size;
370+
ssize_t size;
370371
int status = MMDB_SUCCESS;
371372
HANDLE mmh = NULL;
372373
HANDLE fd = INVALID_HANDLE_VALUE;
@@ -386,12 +387,17 @@ static int map_file(MMDB_s *const mmdb) {
386387
status = MMDB_FILE_OPEN_ERROR;
387388
goto cleanup;
388389
}
389-
size = GetFileSize(fd, NULL);
390-
if (size == INVALID_FILE_SIZE) {
391-
status = MMDB_FILE_OPEN_ERROR;
390+
LARGE_INTEGER file_size;
391+
if (!GetFileSizeEx(fd, &file_size)) {
392+
status = MMDB_IO_ERROR;
393+
goto cleanup;
394+
}
395+
if (file_size.QuadPart < 0 || file_size.QuadPart > SSIZE_MAX) {
396+
status = MMDB_IO_ERROR;
392397
goto cleanup;
393398
}
394-
mmh = CreateFileMapping(fd, NULL, PAGE_READONLY, 0, size, NULL);
399+
size = (ssize_t)file_size.QuadPart;
400+
mmh = CreateFileMapping(fd, NULL, PAGE_READONLY, 0, 0, NULL);
395401
/* Microsoft documentation for CreateFileMapping indicates this returns
396402
NULL not INVALID_HANDLE_VALUE on error */
397403
if (NULL == mmh) {
@@ -882,6 +888,11 @@ MMDB_lookup_result_s MMDB_lookup_string(const MMDB_s *const mmdb,
882888

883889
if (!*gai_error) {
884890
result = MMDB_lookup_sockaddr(mmdb, addresses->ai_addr, mmdb_error);
891+
} else {
892+
/* No MMDB error occurred; the GAI failure is reported via
893+
* *gai_error. Set *mmdb_error to a defined value so callers
894+
* don't read indeterminate memory. */
895+
*mmdb_error = MMDB_SUCCESS;
885896
}
886897

887898
if (NULL != addresses) {
@@ -979,7 +990,7 @@ static int find_address_in_search_tree(const MMDB_s *const mmdb,
979990

980991
result->netmask = current_bit;
981992

982-
if (value >= node_count + mmdb->data_section_size) {
993+
if (value >= (uint64_t)node_count + mmdb->data_section_size) {
983994
// The pointer points off the end of the database.
984995
return MMDB_CORRUPT_SEARCH_TREE_ERROR;
985996
}
@@ -1039,7 +1050,8 @@ static int find_ipv4_start_node(MMDB_s *const mmdb) {
10391050
uint32_t node_count = mmdb->metadata.node_count;
10401051

10411052
for (netmask = 0; netmask < 96 && node_value < node_count; netmask++) {
1042-
record_pointer = &search_tree[node_value * record_info.record_length];
1053+
record_pointer =
1054+
&search_tree[(uint64_t)node_value * record_info.record_length];
10431055
if (record_pointer + record_info.record_length > mmdb->data_section) {
10441056
return MMDB_CORRUPT_SEARCH_TREE_ERROR;
10451057
}
@@ -1097,13 +1109,16 @@ int MMDB_read_node(const MMDB_s *const mmdb,
10971109
return MMDB_UNKNOWN_DATABASE_FORMAT_ERROR;
10981110
}
10991111

1100-
if (node_number > mmdb->metadata.node_count) {
1112+
if (node_number >= mmdb->metadata.node_count) {
11011113
return MMDB_INVALID_NODE_NUMBER_ERROR;
11021114
}
11031115

11041116
const uint8_t *search_tree = mmdb->file_content;
11051117
const uint8_t *record_pointer =
1106-
&search_tree[node_number * record_info.record_length];
1118+
&search_tree[(uint64_t)node_number * record_info.record_length];
1119+
if (record_pointer + record_info.record_length > mmdb->data_section) {
1120+
return MMDB_CORRUPT_SEARCH_TREE_ERROR;
1121+
}
11071122
node->left_record = record_info.left_record_getter(record_pointer);
11081123
record_pointer += record_info.right_record_offset;
11091124
node->right_record = record_info.right_record_getter(record_pointer);
@@ -1272,7 +1287,7 @@ static int lookup_path_in_array(const char *path_elem,
12721287
/* We don't want to follow a pointer here. If the next element is a
12731288
* pointer we simply skip it and keep going */
12741289
CHECKED_DECODE_ONE(mmdb, entry_data->offset_to_next, entry_data);
1275-
int status = skip_map_or_array(mmdb, entry_data);
1290+
int status = skip_map_or_array(mmdb, entry_data, 0);
12761291
if (MMDB_SUCCESS != status) {
12771292
return status;
12781293
}
@@ -1314,7 +1329,7 @@ static int lookup_path_in_map(const char *path_elem,
13141329
/* We don't want to follow a pointer here. If the next element is
13151330
* a pointer we simply skip it and keep going */
13161331
CHECKED_DECODE_ONE(mmdb, offset_to_value, &value);
1317-
int status = skip_map_or_array(mmdb, &value);
1332+
int status = skip_map_or_array(mmdb, &value, 0);
13181333
if (MMDB_SUCCESS != status) {
13191334
return status;
13201335
}
@@ -1327,15 +1342,21 @@ static int lookup_path_in_map(const char *path_elem,
13271342
}
13281343

13291344
static int skip_map_or_array(const MMDB_s *const mmdb,
1330-
MMDB_entry_data_s *entry_data) {
1345+
MMDB_entry_data_s *entry_data,
1346+
int depth) {
1347+
if (depth >= MAXIMUM_DATA_STRUCTURE_DEPTH) {
1348+
DEBUG_MSG("reached the maximum data structure depth");
1349+
return MMDB_INVALID_DATA_ERROR;
1350+
}
1351+
13311352
if (entry_data->type == MMDB_DATA_TYPE_MAP) {
13321353
uint32_t size = entry_data->data_size;
13331354
while (size-- > 0) {
13341355
CHECKED_DECODE_ONE(
13351356
mmdb, entry_data->offset_to_next, entry_data); // key
13361357
CHECKED_DECODE_ONE(
13371358
mmdb, entry_data->offset_to_next, entry_data); // value
1338-
int status = skip_map_or_array(mmdb, entry_data);
1359+
int status = skip_map_or_array(mmdb, entry_data, depth + 1);
13391360
if (MMDB_SUCCESS != status) {
13401361
return status;
13411362
}
@@ -1345,7 +1366,7 @@ static int skip_map_or_array(const MMDB_s *const mmdb,
13451366
while (size-- > 0) {
13461367
CHECKED_DECODE_ONE(
13471368
mmdb, entry_data->offset_to_next, entry_data); // value
1348-
int status = skip_map_or_array(mmdb, entry_data);
1369+
int status = skip_map_or_array(mmdb, entry_data, depth + 1);
13491370
if (MMDB_SUCCESS != status) {
13501371
return status;
13511372
}
@@ -1707,6 +1728,12 @@ static int get_entry_data_list(const MMDB_s *const mmdb,
17071728
case MMDB_DATA_TYPE_ARRAY: {
17081729
uint32_t array_size = entry_data_list->entry_data.data_size;
17091730
uint32_t array_offset = entry_data_list->entry_data.offset_to_next;
1731+
/* Each array element needs at least 1 byte. */
1732+
if (array_offset >= mmdb->data_section_size ||
1733+
array_size > mmdb->data_section_size - array_offset) {
1734+
DEBUG_MSG("array size exceeds remaining data section");
1735+
return MMDB_INVALID_DATA_ERROR;
1736+
}
17101737
while (array_size-- > 0) {
17111738
MMDB_entry_data_list_s *entry_data_list_to =
17121739
data_pool_alloc(pool);
@@ -1730,6 +1757,12 @@ static int get_entry_data_list(const MMDB_s *const mmdb,
17301757
uint32_t size = entry_data_list->entry_data.data_size;
17311758

17321759
offset = entry_data_list->entry_data.offset_to_next;
1760+
/* Each map entry needs at least a key and a value (1 byte each). */
1761+
if (offset >= mmdb->data_section_size ||
1762+
size > (mmdb->data_section_size - offset) / 2) {
1763+
DEBUG_MSG("map size exceeds remaining data section");
1764+
return MMDB_INVALID_DATA_ERROR;
1765+
}
17331766
while (size-- > 0) {
17341767
MMDB_entry_data_list_s *list_key = data_pool_alloc(pool);
17351768
if (!list_key) {
@@ -1894,6 +1927,12 @@ static void free_mmdb_struct(MMDB_s *const mmdb) {
18941927
#pragma clang diagnostic pop
18951928
#endif
18961929
#endif
1930+
mmdb->file_content = NULL;
1931+
mmdb->file_size = 0;
1932+
mmdb->data_section = NULL;
1933+
mmdb->data_section_size = 0;
1934+
mmdb->metadata_section = NULL;
1935+
mmdb->metadata_section_size = 0;
18971936
}
18981937

18991938
if (NULL != mmdb->metadata.database_type) {
@@ -1931,6 +1970,7 @@ static void free_languages_metadata(MMDB_s *mmdb) {
19311970
#endif
19321971
}
19331972
FREE_AND_SET_NULL(mmdb->metadata.languages.names);
1973+
mmdb->metadata.languages.count = 0;
19341974
}
19351975

19361976
static void free_descriptions_metadata(MMDB_s *mmdb) {
@@ -1973,6 +2013,7 @@ static void free_descriptions_metadata(MMDB_s *mmdb) {
19732013
}
19742014

19752015
FREE_AND_SET_NULL(mmdb->metadata.description.descriptions);
2016+
mmdb->metadata.description.count = 0;
19762017
}
19772018

19782019
const char *MMDB_lib_version(void) { return PACKAGE_VERSION; }
@@ -2158,7 +2199,7 @@ dump_entry_data_list(FILE *stream,
21582199

21592200
static void print_indentation(FILE *stream, int i) {
21602201
char buffer[1024];
2161-
int size = i >= 1024 ? 1023 : i;
2202+
int size = i < 0 ? 0 : (i >= 1024 ? 1023 : i);
21622203
memset(buffer, 32, (size_t)size);
21632204
buffer[size] = '\0';
21642205
fputs(buffer, stream);

t/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,33 @@ add_library(tap
55
# test programs
66
set(TEST_TARGET_NAMES
77
bad_pointers_t
8+
bad_search_tree_t
89
basic_lookup_t
910
data_entry_list_t
1011
data-pool-t
1112
data_types_t
13+
double_close_t
1214
dump_t
15+
gai_error_t
1316
get_value_pointer_bug_t
1417
get_value_t
1518
ipv4_start_cache_t
1619
ipv6_lookup_in_ipv4_t
1720
metadata_pointers_t
1821
metadata_t
1922
no_map_get_value_t
23+
overflow_bounds_t
2024
read_node_t
2125
version_t
2226
)
2327

2428
if(UNIX) # or if (NOT WIN32)
2529
list(APPEND TEST_TARGET_NAMES
2630
bad_databases_t
31+
bad_data_size_t
32+
bad_epoch_t
33+
bad_indent_t
34+
max_depth_t
2735
threads_t
2836
)
2937
find_package(Threads)

t/Makefile.am

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ EXTRA_DIST = compile_c++_t.pl external_symbols_t.pl mmdblookup_t.pl \
1616
libtap/tap.c libtap/tap.h maxmind-db
1717

1818
check_PROGRAMS = \
19-
bad_pointers_t bad_databases_t basic_lookup_t data_entry_list_t \
20-
data-pool-t data_types_t dump_t get_value_t get_value_pointer_bug_t \
21-
ipv4_start_cache_t ipv6_lookup_in_ipv4_t metadata_t metadata_pointers_t \
22-
no_map_get_value_t read_node_t threads_t version_t
19+
bad_pointers_t bad_databases_t bad_data_size_t bad_epoch_t bad_indent_t \
20+
bad_search_tree_t \
21+
basic_lookup_t data_entry_list_t \
22+
data-pool-t data_types_t double_close_t dump_t gai_error_t get_value_t \
23+
get_value_pointer_bug_t \
24+
ipv4_start_cache_t ipv6_lookup_in_ipv4_t max_depth_t metadata_t \
25+
metadata_pointers_t no_map_get_value_t overflow_bounds_t read_node_t \
26+
threads_t version_t
2327

2428
data_pool_t_LDFLAGS = $(AM_LDFLAGS) -lm
2529
data_pool_t_SOURCES = data-pool-t.c ../src/data-pool.c

0 commit comments

Comments
 (0)