-
Notifications
You must be signed in to change notification settings - Fork 243
Bound decoded values to prevent a pointer fan-out DoS (STF-1568) #479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
oschwald
wants to merge
8
commits into
main
Choose a base branch
from
greg/stf-1488
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a3c1165
Bound decoded values to prevent a pointer fan-out denial of service
oschwald ec37a55
Bound the total payload decoded for a single entry
oschwald 4771688
Update the test-data submodule for the pointer DoS fixtures
oschwald c487a0a
Add regression tests for the decoder resource limits
oschwald c2bf3ec
fixup! Bound the total payload decoded for a single entry
oschwald cca8b12
fixup! Update the test-data submodule for the pointer DoS fixtures
oschwald 07ab6d6
fixup! Add regression tests for the decoder resource limits
oschwald ca84fcb
fixup! Update the test-data submodule for the pointer DoS fixtures
oschwald File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule maxmind-db
updated
13 files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| #include "maxminddb_test_helper.h" | ||
|
|
||
| // A non-NULL sentinel for the output pointer. On a resource-limit error | ||
| // MMDB_get_entry_data_list must set the caller's output to NULL, not leave a | ||
| // stale pointer that a caller could later free. Starting from this sentinel and | ||
| // asserting the call replaces it with NULL proves that clearing. The node is | ||
| // not heap allocated, so it must never be freed. | ||
| static MMDB_entry_data_list_s sentinel_node; | ||
| #define OUTPUT_SENTINEL (&sentinel_node) | ||
|
|
||
| /* Decoding a crafted fan-out record must be rejected, not run to exhaustion. | ||
| * The value-count and payload byte limits both surface as | ||
| * MMDB_INVALID_DATA_ERROR from MMDB_get_entry_data_list, and the output list | ||
| * must be set to NULL. */ | ||
| static void test_fan_out_rejected(const char *fixture, const char *desc) { | ||
| char *db_file = test_database_path(fixture); | ||
|
|
||
| MMDB_s mmdb; | ||
| int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb); | ||
| cmp_ok(status, "==", MMDB_SUCCESS, "opened %s fixture", desc); | ||
| if (status != MMDB_SUCCESS) { | ||
| diag("MMDB_open failed: %s", MMDB_strerror(status)); | ||
| free(db_file); | ||
| return; | ||
| } | ||
|
|
||
| int gai_error, mmdb_error; | ||
| MMDB_lookup_result_s result = | ||
| MMDB_lookup_string(&mmdb, "1.1.1.1", &gai_error, &mmdb_error); | ||
| cmp_ok(mmdb_error, "==", MMDB_SUCCESS, "%s: lookup succeeded", desc); | ||
| ok(result.found_entry, "%s: entry found", desc); | ||
|
|
||
| if (result.found_entry) { | ||
| MMDB_entry_data_list_s *entry_data_list = OUTPUT_SENTINEL; | ||
| status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); | ||
| cmp_ok(status, | ||
| "==", | ||
| MMDB_INVALID_DATA_ERROR, | ||
| "%s: MMDB_get_entry_data_list returns MMDB_INVALID_DATA_ERROR", | ||
| desc); | ||
| ok(entry_data_list == NULL, | ||
| "%s: output list is set to NULL after the error", | ||
| desc); | ||
| // Free a real partial list, or NULL as a no-op, but never the sentinel. | ||
| if (entry_data_list != OUTPUT_SENTINEL) { | ||
| MMDB_free_entry_data_list(entry_data_list); | ||
| } | ||
| } | ||
|
|
||
| MMDB_close(&mmdb); | ||
| free(db_file); | ||
| } | ||
|
|
||
| /* A normal record must still decode fully. Its payload is far below the limit, | ||
| * so the limits must not reject legitimate data. */ | ||
| static void test_normal_record_allowed(void) { | ||
| char *db_file = test_database_path("GeoIP2-City-Test.mmdb"); | ||
|
|
||
| MMDB_s mmdb; | ||
| int status = MMDB_open(db_file, MMDB_MODE_MMAP, &mmdb); | ||
| cmp_ok(status, "==", MMDB_SUCCESS, "opened GeoIP2-City-Test"); | ||
| if (status != MMDB_SUCCESS) { | ||
| diag("MMDB_open failed: %s", MMDB_strerror(status)); | ||
| free(db_file); | ||
| return; | ||
| } | ||
|
|
||
| int gai_error, mmdb_error; | ||
| MMDB_lookup_result_s result = | ||
| MMDB_lookup_string(&mmdb, "81.2.69.142", &gai_error, &mmdb_error); | ||
| ok(result.found_entry, "normal record: entry found"); | ||
|
|
||
| if (result.found_entry) { | ||
| MMDB_entry_data_list_s *entry_data_list = NULL; | ||
| status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); | ||
| cmp_ok(status, | ||
| "==", | ||
| MMDB_SUCCESS, | ||
| "normal record decodes with no false rejection"); | ||
| ok(entry_data_list != NULL, "normal record: list returned"); | ||
| MMDB_free_entry_data_list(entry_data_list); | ||
| } | ||
|
|
||
| MMDB_close(&mmdb); | ||
| free(db_file); | ||
| } | ||
|
|
||
| /* The counters are per call. A rejected decode must not leave state that | ||
| * changes a later decode on the same reader. */ | ||
| static void test_per_call_state(void) { | ||
| char *dos_file = | ||
| test_database_path("MaxMind-DB-test-payload-amplification-dos.mmdb"); | ||
| MMDB_s dos; | ||
| if (MMDB_open(dos_file, MMDB_MODE_MMAP, &dos) == MMDB_SUCCESS) { | ||
| int gai, err; | ||
| MMDB_lookup_result_s result = | ||
| MMDB_lookup_string(&dos, "1.1.1.1", &gai, &err); | ||
| if (result.found_entry) { | ||
| MMDB_entry_data_list_s *first = OUTPUT_SENTINEL; | ||
|
|
||
| int s1 = MMDB_get_entry_data_list(&result.entry, &first); | ||
| cmp_ok(s1, | ||
| "==", | ||
| MMDB_INVALID_DATA_ERROR, | ||
| "per-call: first decode of the attack record is rejected"); | ||
| if (first != OUTPUT_SENTINEL) { | ||
| MMDB_free_entry_data_list(first); | ||
| } | ||
|
|
||
| MMDB_entry_data_list_s *second = OUTPUT_SENTINEL; | ||
| int s2 = MMDB_get_entry_data_list(&result.entry, &second); | ||
| cmp_ok(s2, | ||
| "==", | ||
| MMDB_INVALID_DATA_ERROR, | ||
| "per-call: repeating it is still rejected, no leaked count"); | ||
| if (second != OUTPUT_SENTINEL) { | ||
| MMDB_free_entry_data_list(second); | ||
| } | ||
|
|
||
| // Same-reader isolation. After the rejections a bounded decode on | ||
| // the same reader must still succeed. Offset 0 is the shared scalar | ||
| // the fan-out points at, a single small value well under the | ||
| // limits. A reader left with exhausted counters would reject it. | ||
| MMDB_entry_s scalar = {.mmdb = &dos, .offset = 0}; | ||
| MMDB_entry_data_list_s *bounded = NULL; | ||
| int s3 = MMDB_get_entry_data_list(&scalar, &bounded); | ||
| cmp_ok(s3, | ||
| "==", | ||
| MMDB_SUCCESS, | ||
| "per-call: a bounded decode on the same reader still works"); | ||
| ok(bounded != NULL, "per-call: bounded decode returned a list"); | ||
| MMDB_free_entry_data_list(bounded); | ||
| } | ||
| MMDB_close(&dos); | ||
| } | ||
| free(dos_file); | ||
| } | ||
|
|
||
| int main(void) { | ||
| plan(NO_PLAN); | ||
| /* Value-count limit: nested arrays of pointers to shared targets. */ | ||
| test_fan_out_rejected("MaxMind-DB-test-pointer-decoder-dos.mmdb", | ||
| "value-count fan-out"); | ||
| /* Payload byte limit: many pointers to one large bytes value. */ | ||
| test_fan_out_rejected("MaxMind-DB-test-payload-amplification-dos.mmdb", | ||
| "payload amplification"); | ||
| /* Worst case under the value-count limit, caught only by the byte limit. */ | ||
| test_fan_out_rejected( | ||
| "MaxMind-DB-test-payload-amplification-dos-worst-case.mmdb", | ||
| "worst-case payload"); | ||
| /* Payload byte limit via a shared UTF-8 string, the type bindings copy. */ | ||
| test_fan_out_rejected( | ||
| "MaxMind-DB-test-payload-amplification-dos-string.mmdb", | ||
| "string payload amplification"); | ||
| test_normal_record_allowed(); | ||
| test_per_call_state(); | ||
| done_testing(); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Hyphenate “denial-of-service.”
Change
payload-amplification denial of servicetopayload-amplification denial-of-service issuefor consistent compound-word usage.Proposed wording
📝 Committable suggestion
🧰 Tools
🪛 LanguageTool
[grammar] ~11-~11: Use a hyphen to join words.
Context: ...d a related payload-amplification denial of service. A crafted database can point ...
(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
Source: Linters/SAST tools