|
| 1 | +#include "maxminddb_test_helper.h" |
| 2 | + |
| 3 | +/* Decoding a crafted fan-out record must be rejected, not run to exhaustion. |
| 4 | + * The value-count and payload byte limits both surface as |
| 5 | + * MMDB_INVALID_DATA_ERROR from MMDB_get_entry_data_list, and the output list |
| 6 | + * must be left NULL. */ |
| 7 | +static void test_fan_out_rejected(const char *fixture, const char *desc) { |
| 8 | + char *db_file = test_database_path(fixture); |
| 9 | + |
| 10 | + MMDB_s mmdb; |
| 11 | + int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb); |
| 12 | + cmp_ok(status, "==", MMDB_SUCCESS, "opened %s fixture", desc); |
| 13 | + if (status != MMDB_SUCCESS) { |
| 14 | + diag("MMDB_open failed: %s", MMDB_strerror(status)); |
| 15 | + free(db_file); |
| 16 | + return; |
| 17 | + } |
| 18 | + |
| 19 | + int gai_error, mmdb_error; |
| 20 | + MMDB_lookup_result_s result = |
| 21 | + MMDB_lookup_string(&mmdb, "1.1.1.1", &gai_error, &mmdb_error); |
| 22 | + cmp_ok(mmdb_error, "==", MMDB_SUCCESS, "%s: lookup succeeded", desc); |
| 23 | + ok(result.found_entry, "%s: entry found", desc); |
| 24 | + |
| 25 | + if (result.found_entry) { |
| 26 | + MMDB_entry_data_list_s *entry_data_list = NULL; |
| 27 | + status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); |
| 28 | + cmp_ok(status, |
| 29 | + "==", |
| 30 | + MMDB_INVALID_DATA_ERROR, |
| 31 | + "%s: MMDB_get_entry_data_list returns MMDB_INVALID_DATA_ERROR", |
| 32 | + desc); |
| 33 | + ok(entry_data_list == NULL, |
| 34 | + "%s: output list is NULL after the error", |
| 35 | + desc); |
| 36 | + MMDB_free_entry_data_list(entry_data_list); |
| 37 | + } |
| 38 | + |
| 39 | + MMDB_close(&mmdb); |
| 40 | + free(db_file); |
| 41 | +} |
| 42 | + |
| 43 | +/* A normal record must still decode fully. Its payload is far below the limit, |
| 44 | + * so the limits must not reject legitimate data. */ |
| 45 | +static void test_normal_record_allowed(void) { |
| 46 | + char *db_file = test_database_path("GeoIP2-City-Test.mmdb"); |
| 47 | + |
| 48 | + MMDB_s mmdb; |
| 49 | + int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb); |
| 50 | + cmp_ok(status, "==", MMDB_SUCCESS, "opened GeoIP2-City-Test"); |
| 51 | + if (status != MMDB_SUCCESS) { |
| 52 | + diag("MMDB_open failed: %s", MMDB_strerror(status)); |
| 53 | + free(db_file); |
| 54 | + return; |
| 55 | + } |
| 56 | + |
| 57 | + int gai_error, mmdb_error; |
| 58 | + MMDB_lookup_result_s result = |
| 59 | + MMDB_lookup_string(&mmdb, "81.2.69.142", &gai_error, &mmdb_error); |
| 60 | + ok(result.found_entry, "normal record: entry found"); |
| 61 | + |
| 62 | + if (result.found_entry) { |
| 63 | + MMDB_entry_data_list_s *entry_data_list = NULL; |
| 64 | + status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); |
| 65 | + cmp_ok(status, |
| 66 | + "==", |
| 67 | + MMDB_SUCCESS, |
| 68 | + "normal record decodes with no false rejection"); |
| 69 | + ok(entry_data_list != NULL, "normal record: list returned"); |
| 70 | + MMDB_free_entry_data_list(entry_data_list); |
| 71 | + } |
| 72 | + |
| 73 | + MMDB_close(&mmdb); |
| 74 | + free(db_file); |
| 75 | +} |
| 76 | + |
| 77 | +/* The counters are per call. A rejected decode must not leave state that |
| 78 | + * changes a later decode, on the same reader or another one. */ |
| 79 | +static void test_per_call_state(void) { |
| 80 | + char *dos_file = |
| 81 | + test_database_path("MaxMind-DB-test-payload-amplification-dos.mmdb"); |
| 82 | + MMDB_s dos; |
| 83 | + if (MMDB_open(dos_file, MMDB_MODE_MMAP, &dos) == MMDB_SUCCESS) { |
| 84 | + int gai, err; |
| 85 | + MMDB_lookup_result_s result = |
| 86 | + MMDB_lookup_string(&dos, "1.1.1.1", &gai, &err); |
| 87 | + if (result.found_entry) { |
| 88 | + MMDB_entry_data_list_s *first = NULL; |
| 89 | + int s1 = MMDB_get_entry_data_list(&result.entry, &first); |
| 90 | + cmp_ok(s1, |
| 91 | + "==", |
| 92 | + MMDB_INVALID_DATA_ERROR, |
| 93 | + "per-call: first decode of the attack record is rejected"); |
| 94 | + MMDB_free_entry_data_list(first); |
| 95 | + |
| 96 | + MMDB_entry_data_list_s *second = NULL; |
| 97 | + int s2 = MMDB_get_entry_data_list(&result.entry, &second); |
| 98 | + cmp_ok(s2, |
| 99 | + "==", |
| 100 | + MMDB_INVALID_DATA_ERROR, |
| 101 | + "per-call: repeating it is still rejected, no leaked count"); |
| 102 | + MMDB_free_entry_data_list(second); |
| 103 | + } |
| 104 | + MMDB_close(&dos); |
| 105 | + } |
| 106 | + free(dos_file); |
| 107 | + |
| 108 | + char *ok_file = test_database_path("GeoIP2-City-Test.mmdb"); |
| 109 | + MMDB_s good; |
| 110 | + if (MMDB_open(ok_file, MMDB_MODE_MMAP, &good) == MMDB_SUCCESS) { |
| 111 | + int gai, err; |
| 112 | + MMDB_lookup_result_s result = |
| 113 | + MMDB_lookup_string(&good, "81.2.69.142", &gai, &err); |
| 114 | + if (result.found_entry) { |
| 115 | + MMDB_entry_data_list_s *list = NULL; |
| 116 | + int status = MMDB_get_entry_data_list(&result.entry, &list); |
| 117 | + cmp_ok(status, |
| 118 | + "==", |
| 119 | + MMDB_SUCCESS, |
| 120 | + "per-call: a valid decode after a rejected one still works"); |
| 121 | + MMDB_free_entry_data_list(list); |
| 122 | + } |
| 123 | + MMDB_close(&good); |
| 124 | + } |
| 125 | + free(ok_file); |
| 126 | +} |
| 127 | + |
| 128 | +int main(void) { |
| 129 | + plan(NO_PLAN); |
| 130 | + /* Value-count limit: nested arrays of pointers to shared targets. */ |
| 131 | + test_fan_out_rejected("MaxMind-DB-test-pointer-decoder-dos.mmdb", |
| 132 | + "value-count fan-out"); |
| 133 | + /* Payload byte limit: many pointers to one large string. */ |
| 134 | + test_fan_out_rejected("MaxMind-DB-test-payload-amplification-dos.mmdb", |
| 135 | + "payload amplification"); |
| 136 | + /* Worst case under the value-count limit, caught only by the byte limit. */ |
| 137 | + test_fan_out_rejected( |
| 138 | + "MaxMind-DB-test-payload-amplification-dos-worst-case.mmdb", |
| 139 | + "worst-case payload"); |
| 140 | + test_normal_record_allowed(); |
| 141 | + test_per_call_state(); |
| 142 | + done_testing(); |
| 143 | +} |
0 commit comments