Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## next release

- `MMDB_get_entry_data_list()` now returns `MMDB_INVALID_DATA_ERROR` when a map
key is not a UTF-8 string, as the MaxMind DB specification requires.
Previously, it added the key to the list with its original type. A caller that
read the key as a string read the wrong union member. From this version on,
code that walks the list can rely on every map key being a
`MMDB_DATA_TYPE_UTF8_STRING` entry. `MMDB_get_metadata_as_entry_data_list()`
has the same check. `MMDB_open()` still returns `MMDB_INVALID_METADATA_ERROR`
when the `description` or `languages` metadata contains a map with a
non-string key.

## 1.14.1 - 2026-09-17

- Fixed a `-Wsign-compare` warning in `MMDB_open()` that broke builds with
Expand Down
10 changes: 9 additions & 1 deletion doc/libmaxminddb.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ status codes are:
- `MMDB_INVALID_DATA_ERROR` - an entry in the data section contains invalid
data. For example, a `uint16` field is claiming to be more than 2 bytes long.
The database is probably damaged or was generated incorrectly.
`MMDB_get_entry_data_list()` also returns this error when a map key is not a
UTF-8 string.
- `MMDB_INVALID_LOOKUP_PATH_ERROR` - The lookup path passed to `MMDB_get_value`,
`MMDB_vget_value`, or `MMDB_aget_value` contains an array offset that is
larger than LONG_MAX or smaller than LONG_MIN.
Expand Down Expand Up @@ -457,7 +459,9 @@ releases. The current default is `MMDB_MODE_MMAP`.

If metadata processing exceeds one of the decoder resource limits described
under `MMDB_get_entry_data_list()`, this function returns
`MMDB_INVALID_METADATA_ERROR`.
`MMDB_INVALID_METADATA_ERROR`. It also returns `MMDB_INVALID_METADATA_ERROR`
when the `description` or `languages` metadata contains a map with a non-string
key.

## `MMDB_close()`

Expand Down Expand Up @@ -647,6 +651,8 @@ This function allows you to get all of the data for a complex data structure at
once, rather than looking up each piece using repeated calls to
`MMDB_get_value()`.

A map key that is not a string returns `MMDB_INVALID_DATA_ERROR`.

A crafted database can make a full decode expensive, so this function bounds the
work and the caller-visible payload. Each call decodes at most 65,536 list
values and 2 MiB of UTF-8 string and bytes payload. A structure exactly at
Expand Down Expand Up @@ -752,6 +758,8 @@ This function allows you to retrieve the database metadata as a linked list of
with the metadata than using the metadata structure directly. It uses the same
per-call limits as `MMDB_get_entry_data_list()` and returns
`MMDB_DECODER_LIMIT_ERROR` if the complete metadata list exceeds any of them.
Like that function, it returns `MMDB_INVALID_DATA_ERROR` when a map key is not a
string.

```c
MMDB_entry_data_list_s *entry_data_list, *first;
Expand Down
35 changes: 27 additions & 8 deletions src/maxminddb.c
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ typedef struct record_info_s {
typedef struct decode_state_s {
size_t values;
uint64_t bytes;
bool allow_non_string_keys;
} decode_state_s;

#define METADATA_MARKER "\xab\xcd\xefMaxMind.com"
Expand Down Expand Up @@ -236,6 +237,9 @@ static int decode_one(const MMDB_s *const mmdb,
static int get_ext_type(int raw_ext_type);
static uint32_t
get_ptr_from(uint8_t ctrl, uint8_t const *const ptr, int ptr_size);
static int entry_data_list_for(MMDB_entry_s *start,
MMDB_entry_data_list_s **const entry_data_list,
bool allow_non_string_keys);
static int get_entry_data_list(const MMDB_s *const mmdb,
uint32_t offset,
MMDB_entry_data_list_s *const entry_data_list,
Expand Down Expand Up @@ -791,8 +795,10 @@ static int populate_languages_metadata(MMDB_s *mmdb,
MMDB_entry_s array_start = {.mmdb = metadata_db,
.offset = entry_data.offset};

// The type checks below report a non-string key in the languages value as
// MMDB_INVALID_METADATA_ERROR.
MMDB_entry_data_list_s *member;
status = MMDB_get_entry_data_list(&array_start, &member);
status = entry_data_list_for(&array_start, &member, true);
if (MMDB_SUCCESS != status) {
return status;
}
Expand Down Expand Up @@ -852,14 +858,15 @@ static int populate_description_metadata(MMDB_s *mmdb,

MMDB_entry_s map_start = {.mmdb = metadata_db, .offset = entry_data.offset};

// The type checks below report a non-string key in the description value
// as MMDB_INVALID_METADATA_ERROR.
MMDB_entry_data_list_s *member;
status = MMDB_get_entry_data_list(&map_start, &member);
status = entry_data_list_for(&map_start, &member, true);
if (MMDB_SUCCESS != status) {
DEBUG_MSGF(
"MMDB_get_entry_data_list failed while populating description."
" status = %d (%s)",
status,
MMDB_strerror(status));
DEBUG_MSGF("entry_data_list_for failed while populating description."
" status = %d (%s)",
status,
MMDB_strerror(status));
return status;
}

Expand Down Expand Up @@ -1742,6 +1749,12 @@ int MMDB_get_metadata_as_entry_data_list(

int MMDB_get_entry_data_list(MMDB_entry_s *start,
MMDB_entry_data_list_s **const entry_data_list) {
return entry_data_list_for(start, entry_data_list, false);
}

static int entry_data_list_for(MMDB_entry_s *start,
MMDB_entry_data_list_s **const entry_data_list,
bool allow_non_string_keys) {
*entry_data_list = NULL;

size_t const maximum_values = (size_t)(MAXIMUM_DATA_STRUCTURE_VALUES);
Expand All @@ -1751,7 +1764,8 @@ int MMDB_get_entry_data_list(MMDB_entry_s *start,
return MMDB_OUT_OF_MEMORY_ERROR;
}

decode_state_s decode_state = {0};
decode_state_s decode_state = {.allow_non_string_keys =
allow_non_string_keys};
MMDB_entry_data_list_s *list = NULL;
int status = alloc_entry_data_list(pool, &decode_state, &list);
if (MMDB_SUCCESS != status) {
Expand Down Expand Up @@ -1894,6 +1908,11 @@ static int get_entry_data_list(const MMDB_s *const mmdb,
DEBUG_MSG("get_entry_data_list on map key failed.");
return status;
}
if (!decode_state->allow_non_string_keys &&
MMDB_DATA_TYPE_UTF8_STRING != list_key->entry_data.type) {
DEBUG_MSG("map key is not a UTF-8 string");
return MMDB_INVALID_DATA_ERROR;
}

offset = list_key->entry_data.offset_to_next;

Expand Down
1 change: 1 addition & 0 deletions t/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ set(TEST_TARGET_NAMES
metadata_pointers_t
metadata_t
no_map_get_value_t
non_string_map_key_t
overflow_bounds_t
pointer_dos_t
read_node_t
Expand Down
2 changes: 1 addition & 1 deletion t/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ check_PROGRAMS = \
get_value_pointer_bug_t invalid_sockaddr_t \
ipv4_start_cache_t ipv6_lookup_in_ipv4_t max_depth_t metadata_t \
metadata_marker_t metadata_pointers_t no_map_get_value_t \
overflow_bounds_t pointer_dos_t read_node_t \
non_string_map_key_t overflow_bounds_t pointer_dos_t read_node_t \
threads_t version_t

data_pool_t_LDFLAGS = $(AM_LDFLAGS) -lm
Expand Down
199 changes: 199 additions & 0 deletions t/non_string_map_key_t.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#include "maxminddb_test_helper.h"
#ifndef _WIN32
#include <unistd.h>
#endif

static void check_entry_data_list(const char *section,
size_t size,
int expected_status,
const char *message) {
// An exact-size heap buffer lets a sanitizer report any read past its end.
uint8_t *data = malloc(size);
if (NULL == data) {
BAIL_OUT("could not allocate section");
}
memcpy(data, section, size);

MMDB_s mmdb = {.data_section = data, .data_section_size = (uint32_t)size};
MMDB_entry_s entry = {.mmdb = &mmdb, .offset = 0};
MMDB_entry_data_list_s *entry_data_list = NULL;

int status = MMDB_get_entry_data_list(&entry, &entry_data_list);
cmp_ok(status, "==", expected_status, "status: %s", message);
if (MMDB_SUCCESS == status) {
MMDB_entry_data_list_s *key = entry_data_list->next;
if (cmp_ok(key->entry_data.type,
"==",
MMDB_DATA_TYPE_UTF8_STRING,
"key type: %s",
message)) {
ok(key->entry_data.data_size == 1 &&
key->entry_data.utf8_string[0] == 'k',
"key value: %s",
message);
}
} else {
ok(NULL == entry_data_list, "list is NULL: %s", message);
}

MMDB_free_entry_data_list(entry_data_list);
free(data);
}

#define CHECK_ENTRY_DATA_LIST(section, status, message) \
check_entry_data_list(section, sizeof(section) - 1, status, message)

static void check_metadata_as_entry_data_list(void) {
// {1: "x"}, where the key is a uint16.
uint8_t metadata[] = {0xE1, 0xA1, 0x01, 0x41, 'x'};
MMDB_s mmdb = {.metadata_section = metadata,
.metadata_section_size = sizeof(metadata)};
MMDB_entry_data_list_s *entry_data_list = NULL;

int status = MMDB_get_metadata_as_entry_data_list(&mmdb, &entry_data_list);
cmp_ok(status,
"==",
MMDB_INVALID_DATA_ERROR,
"MMDB_get_metadata_as_entry_data_list rejects a uint16 key");
MMDB_free_entry_data_list(entry_data_list);
}

#ifndef _WIN32
// A database with one node whose records point to no data, a 4-byte data
// section, and the metadata marker. The metadata follows it.
#define DATABASE_PREFIX \
"\x00\x00\x01\x00\x00\x01" \
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
"\x43" \
"abc" \
"\xAB\xCD\xEF" \
"MaxMind.com"

#define METADATA(languages, description) \
"\xE9" \
"\x4A" \
"node_count" \
"\xC1\x01" \
"\x4B" \
"record_size" \
"\xA1\x18" \
"\x4A" \
"ip_version" \
"\xA1\x04" \
"\x4D" \
"database_type" \
"\x44" \
"Test" \
"\x5B" \
"binary_format_major_version" \
"\xA1\x02" \
"\x5B" \
"binary_format_minor_version" \
"\xA0" \
"\x4B" \
"build_epoch" \
"\x01\x02\x01" \
"\x49" \
"languages" languages "\x4B" \
"description" description

static void check_open(const char *database,
size_t size,
int expected_status,
const char *message) {
char filename[64];
snprintf(filename,
sizeof(filename),
"/tmp/non_string_map_key_t.%ld",
(long)getpid());
FILE *fp = fopen(filename, "wbx");
if (NULL == fp) {
BAIL_OUT("could not create %s", filename);
}
size_t written = fwrite(database, 1, size, fp);
if (0 != fclose(fp) || written != size) {
BAIL_OUT("could not write the temporary file");
}

MMDB_s mmdb;
int status = MMDB_open(filename, MMDB_MODE_MMAP, &mmdb);
cmp_ok(status, "==", expected_status, "MMDB_open status: %s", message);
if (MMDB_SUCCESS == status) {
MMDB_close(&mmdb);
}
unlink(filename);
}

#define CHECK_OPEN(database, status, message) \
check_open(database, sizeof(database) - 1, status, message)
#endif

int main(void) {
plan(NO_PLAN);

// {"k": "x"}
CHECK_ENTRY_DATA_LIST("\xE1"
"\x41"
"k"
"\x41"
"x",
MMDB_SUCCESS,
"a string key is accepted");

// {1: "x"}, where the key is a uint16.
CHECK_ENTRY_DATA_LIST("\xE1"
"\xA1\x01"
"\x41"
"x",
MMDB_INVALID_DATA_ERROR,
"a uint16 key is rejected");

// A map whose key is a pointer to offset 5, followed by the value "x".
// Offset 5 holds the string "k".
CHECK_ENTRY_DATA_LIST("\xE1"
"\x20\x05"
"\x41"
"x"
"\x41"
"k",
MMDB_SUCCESS,
"a key that points to a string is accepted");

// As above, but offset 5 holds the uint16 1.
CHECK_ENTRY_DATA_LIST("\xE1"
"\x20\x05"
"\x41"
"x"
"\xA1\x01",
MMDB_INVALID_DATA_ERROR,
"a key that points to a uint16 is rejected");

check_metadata_as_entry_data_list();

#ifndef _WIN32
// An empty array is "\x00\x04", and an empty map is "\xE0".
CHECK_OPEN(DATABASE_PREFIX METADATA("\x00\x04", "\xE0"),
MMDB_SUCCESS,
"valid metadata");

// MMDB_open() reports a non-string key in the description or languages
// metadata as invalid metadata, not as invalid data.
CHECK_OPEN(DATABASE_PREFIX METADATA("\x00\x04",
"\xE1"
"\xA1\x01"
"\x41"
"x"),
MMDB_INVALID_METADATA_ERROR,
"a uint16 key in description");
CHECK_OPEN(DATABASE_PREFIX METADATA("\x01\x04"
"\xE1"
"\xA1\x01"
"\x41"
"x",
"\xE0"),
MMDB_INVALID_METADATA_ERROR,
"a map with a uint16 key in languages");
#endif

done_testing();
}
Loading