Version in base suite: 1.13.2-1+deb13u1 Base version: libmongocrypt_1.13.2-1+deb13u1 Target version: libmongocrypt_1.13.2-1+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/libm/libmongocrypt/libmongocrypt_1.13.2-1+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/libm/libmongocrypt/libmongocrypt_1.13.2-1+deb13u2.dsc changelog | 15 patches/0002_CVE-2026-84962.patch | 143 +++ patches/0003_CVE-2026-84971.patch | 281 +++++++ patches/0004_CVE-2026-106429_1_of_3.patch | 97 ++ patches/0005_CVE-2026-106429_2_of_3.patch | 353 +++++++++ patches/0006_CVE-2026-106429_3_of_3.patch | 22 patches/0007_CVE-2026-106434_1_of_2.patch | 111 ++ patches/0008_CVE-2026-106434_2_of_2.patch | 194 ++++ patches/0009_CVE-2026-106433_1_of_2.patch | 71 + patches/0010_CVE-2026-106433_2_of_2.patch | 138 +++ patches/0011_CVE-2026-114655.patch | 1169 ++++++++++++++++++++++++++++++ patches/series | 10 12 files changed, 2604 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpenleseld/libmongocrypt_1.13.2-1+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpenleseld/libmongocrypt_1.13.2-1+deb13u2.dsc: no acceptable signature found diff -Nru libmongocrypt-1.13.2/debian/changelog libmongocrypt-1.13.2/debian/changelog --- libmongocrypt-1.13.2/debian/changelog 2026-08-29 15:20:33.000000000 +0000 +++ libmongocrypt-1.13.2/debian/changelog 2026-11-03 20:28:53.000000000 +0000 @@ -1,3 +1,18 @@ +libmongocrypt (1.13.2-1+deb13u2) trixie; urgency=medium + + * Fix CVE-2026-84962: authenticated KMS request forgery via CRLF injection + in GCP key identifier strings + * Fix CVE-2026-84971: reject undersized ciphertext before allocating + plaintext + * Fix CVE-2026-106429: application denial of service via malformed KMS + endpoint + * Fix CVE-2026-106434: unrecognized payload acceptance in explicit decryption + * Fix CVE-2026-106433: heap corruption via duplicate masterKey fields + * Fix CVE-2026-114655: KMS request injection via unsanitized key vault and + provider values + + -- Roberto C. Sanchez Tue, 03 Nov 2026 15:28:53 -0500 + libmongocrypt (1.13.2-1+deb13u1) trixie; urgency=medium * Fix CVE-2026-81523: validate db and collection names diff -Nru libmongocrypt-1.13.2/debian/patches/0002_CVE-2026-84962.patch libmongocrypt-1.13.2/debian/patches/0002_CVE-2026-84962.patch --- libmongocrypt-1.13.2/debian/patches/0002_CVE-2026-84962.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0002_CVE-2026-84962.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,143 @@ +From 53b74103284cbdf035d97f177a796093c95525f5 Mon Sep 17 00:00:00 2001 +From: Kevin Albertson +Date: Thu, 6 Aug 2026 09:55:36 -0400 +Subject: [PATCH] MONGOCRYPT-960 further validate GCP fields (#1210) + +--- + kms-message/src/kms_gcp_request.c | 42 +++++++++++++++++++++++++ + test/test-gcp-auth.c | 51 +++++++++++++++++++++++++++++++ + 2 files changed, 93 insertions(+) + +diff --git a/kms-message/src/kms_gcp_request.c b/kms-message/src/kms_gcp_request.c +index b2111db9..04d1b112 100644 +--- a/kms-message/src/kms_gcp_request.c ++++ b/kms-message/src/kms_gcp_request.c +@@ -20,6 +20,8 @@ + #include "kms_message_private.h" + #include "kms_request_opt_private.h" + ++#include ++ + /* Set a default expiration of 5 minutes for JSON Web Tokens (GCP allows up to + * one hour) */ + #define JWT_EXPIRATION_SECS 5 * 60 +@@ -145,6 +147,38 @@ done: + return req; + } + ++/* _check_key_identifier returns true if `value` is safe to use as a URL path segment. ++ * ++ * Key identifiers may originate from key vault documents or caller-supplied options. They are ++ * formatted into the request path, so an unescaped '/', '?', '#', space, or CR/LF would rewrite ++ * the request target or inject header lines into the request. ++ * ++ * Only the RFC 3986 unreserved characters are permitted. This is a superset of what GCP itself ++ * accepts, so it does not reject any identifier that would otherwise work: ++ * - keyRing and keyName must match `[a-zA-Z0-9_-]{1,63}`. See ++ * https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings/create and ++ * https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/create ++ * - projectId is 6-30 characters of lowercase letters, numbers, and hyphens. See ++ * https://cloud.google.com/resource-manager/docs/creating-managing-projects ++ * - location is a Cloud location name (e.g. "global", "us-east1"). ++ * - keyVersion is a positive integer. ++ * ++ * `value` is not included in the error message: it is untrusted, and the error may be logged. */ ++static bool ++_check_key_identifier (kms_request_t *req, const char *name, const char *value) ++{ ++ const char *c; ++ ++ for (c = value; *c != '\0'; c++) { ++ if (!isalnum ((unsigned char) *c) && *c != '-' && *c != '.' && *c != '_' && ++ *c != '~') { ++ KMS_ERROR (req, "Invalid character in GCP KMS key identifier: %s", name); ++ return false; ++ } ++ } ++ return true; ++} ++ + static kms_request_t * + _encrypt_decrypt_common (const char *encrypt_decrypt, + const char *host, +@@ -192,6 +226,14 @@ _encrypt_decrypt_common (const char *encrypt_decrypt, + goto done; + } + ++ if (!_check_key_identifier (req, "projectId", project_id) || ++ !_check_key_identifier (req, "location", location) || ++ !_check_key_identifier (req, "keyRing", key_ring_name) || ++ !_check_key_identifier (req, "keyName", key_name) || ++ (key_version && !_check_key_identifier (req, "keyVersion", key_version))) { ++ goto done; ++ } ++ + value_base64 = kms_message_raw_to_b64 (value, value_len); + if (!value_base64) { + KMS_ERROR (req, "Could not bases64-encode plaintext"); +diff --git a/test/test-gcp-auth.c b/test/test-gcp-auth.c +index 02f0d016..f582671f 100644 +--- a/test/test-gcp-auth.c ++++ b/test/test-gcp-auth.c +@@ -242,7 +242,58 @@ static void _test_encrypt_with_accesstoken(_mongocrypt_tester_t *tester) { + mongocrypt_destroy(crypt); + } + ++// Test a CRLF in a key vault document's keyName. Regression test for MONGOCRYPT-960. ++static void _test_crlf_in_gcp_masterkey(_mongocrypt_tester_t *tester) { ++ mongocrypt_t *crypt; ++ mongocrypt_ctx_t *ctx; ++ mongocrypt_binary_t *uuid; ++ const char *uuid_data = "\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"; ++ ++ // A key vault document whose masterKey.keyName contains a CRLF. ++ // clang-format off ++ const char *key_doc = BSON_STR({ ++ "status" : 1, ++ "_id" : {"$binary" : {"base64" : "YWFhYWFhYWFhYWFhYWFhYQ==", "subType" : "04"}}, ++ "masterKey" : { ++ "provider" : "gcp", ++ "projectId" : "test", ++ "location" : "global", ++ "keyRing" : "test", ++ "keyName" : "test\r\nFOOBAR: injected" ++ }, ++ "updateDate" : {"$date" : {"$numberLong" : "1557827033449"}}, ++ "creationDate" : {"$date" : {"$numberLong" : "1557827033449"}}, ++ "keyMaterial" : { ++ "$binary" : { ++ "base64" : "PwwOESbKs57YTJtGSCsuAJbv9VWRHjLdPdziUVxH0K9woZUka4SghSwJlw9n6TU/dYHFODvSa4p7bfKzS6U8kGFvOnd7LcPU63IkTek4qZEbGpTRwI5lMT4FFWdpYpUf", ++ "subType" : "00" ++ } ++ } ++ }); ++ // clang-format on ++ ++ crypt = mongocrypt_new(); ++ ASSERT_OK(mongocrypt_setopt_kms_providers(crypt, TEST_BSON("{'gcp': {'accessToken': 'foobar'}}")), crypt); ++ ASSERT_OK(_mongocrypt_init_for_test(crypt), crypt); ++ ctx = mongocrypt_ctx_new(crypt); ++ uuid = mongocrypt_binary_new_from_data((uint8_t *)uuid_data, UUID_LEN); ++ ASSERT_OK(mongocrypt_ctx_setopt_key_id(ctx, uuid), ctx); ++ ASSERT_OK(mongocrypt_ctx_setopt_algorithm(ctx, MONGOCRYPT_ALGORITHM_DETERMINISTIC_STR, -1), ctx); ++ ASSERT_OK(mongocrypt_ctx_explicit_encrypt_init(ctx, TEST_BSON("{'v': 1}")), ctx); ++ ++ ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_MONGO_KEYS); ++ /* Constructing the KMS request fails. No request is sent. */ ++ ASSERT_FAILS(mongocrypt_ctx_mongo_feed(ctx, TEST_BSON(key_doc)), ++ ctx, ++ "Invalid character in GCP KMS key identifier: keyName"); ++ ++ mongocrypt_binary_destroy(uuid); ++ mongocrypt_ctx_destroy(ctx); ++ mongocrypt_destroy(crypt); ++} ++ + void _mongocrypt_tester_install_gcp_auth(_mongocrypt_tester_t *tester) { ++ INSTALL_TEST(_test_crlf_in_gcp_masterkey); + INSTALL_TEST(_test_createdatakey_with_credentials); + INSTALL_TEST(_test_encrypt_with_credentials); + INSTALL_TEST(_test_createdatakey_with_accesstoken); +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0003_CVE-2026-84971.patch libmongocrypt-1.13.2/debian/patches/0003_CVE-2026-84971.patch --- libmongocrypt-1.13.2/debian/patches/0003_CVE-2026-84971.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0003_CVE-2026-84971.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,281 @@ +From 02a0bb0178270c729a0a11f29b6f637870b287ea Mon Sep 17 00:00:00 2001 +From: mdb-ad <198671546+mdb-ad@users.noreply.github.com> +Date: Wed, 2 Sep 2026 15:03:39 -0400 +Subject: [PATCH] MONGOCRYPT-971 reject undersized ciphertext before allocating + plaintext + +--- + src/mc-fle2-insert-update-payload-v2.c | 7 ++- + src/mc-fle2-insert-update-payload.c | 7 ++- + src/mc-fle2-payload-iev.c | 12 ++++- + src/mc-fle2-payload-uev-common.c | 2 + src/mongocrypt-crypto.c | 10 +++- + src/mongocrypt-ctx-decrypt.c | 2 + test/test-mc-fle2-payload-iup-v2.c | 64 +++++++++++++++++++++++++++++++ + test/test-mc-fle2-payload-iup.c | 67 +++++++++++++++++++++++++++++++++ + 8 files changed, 162 insertions(+), 9 deletions(-) + +--- a/src/mc-fle2-insert-update-payload-v2.c ++++ b/src/mc-fle2-insert-update-payload-v2.c +@@ -535,7 +535,12 @@ + return NULL; + } + +- _mongocrypt_buffer_resize(&iup->plaintext, fle2v2->get_plaintext_len(ciphertext.len, status)); ++ const uint32_t plaintext_len = fle2v2->get_plaintext_len(ciphertext.len, status); ++ if (!mongocrypt_status_ok(status)) { ++ return NULL; ++ } ++ ++ _mongocrypt_buffer_resize(&iup->plaintext, plaintext_len); + uint32_t bytes_written; + + if (!fle2v2->do_decrypt(crypto, &iup->userKeyId, user_key, &ciphertext, &iup->plaintext, &bytes_written, status)) { +--- a/src/mc-fle2-insert-update-payload.c ++++ b/src/mc-fle2-insert-update-payload.c +@@ -263,7 +263,12 @@ + return NULL; + } + +- _mongocrypt_buffer_resize(&iup->plaintext, fle2aead->get_plaintext_len(ciphertext.len, status)); ++ const uint32_t plaintext_len = fle2aead->get_plaintext_len(ciphertext.len, status); ++ if (!mongocrypt_status_ok(status)) { ++ return NULL; ++ } ++ ++ _mongocrypt_buffer_resize(&iup->plaintext, plaintext_len); + uint32_t bytes_written; /* ignored */ + + if (!fle2aead +--- a/src/mc-fle2-payload-iev.c ++++ b/src/mc-fle2-payload-iev.c +@@ -350,7 +350,11 @@ + const _mongocrypt_buffer_t *token_buf = mc_ServerDataEncryptionLevel1Token_get(token); + uint32_t bytes_written; + +- _mongocrypt_buffer_resize(&iev->Inner, fle2alg->get_plaintext_len(iev->InnerEncrypted.len, status)); ++ const uint32_t inner_len = fle2alg->get_plaintext_len(iev->InnerEncrypted.len, status); ++ if (!mongocrypt_status_ok(status)) { ++ return false; ++ } ++ _mongocrypt_buffer_resize(&iev->Inner, inner_len); + + /* Decrypt InnerEncrypted. */ + if (!fle2alg->do_decrypt(crypto, +@@ -442,7 +446,11 @@ + return false; + } + /* Attempt to decrypt ClientEncryptedValue */ +- _mongocrypt_buffer_resize(&iev->ClientValue, fle2aead->get_plaintext_len(iev->ClientEncryptedValue.len, status)); ++ const uint32_t client_value_len = fle2aead->get_plaintext_len(iev->ClientEncryptedValue.len, status); ++ if (!mongocrypt_status_ok(status)) { ++ return false; ++ } ++ _mongocrypt_buffer_resize(&iev->ClientValue, client_value_len); + uint32_t bytes_written; + if (!fle2aead->do_decrypt(crypto, + &iev->K_KeyId, +--- a/src/mc-fle2-payload-uev-common.c ++++ b/src/mc-fle2-payload-uev-common.c +@@ -92,7 +92,7 @@ + memcpy(AD.data + 1, key_uuid->data, key_uuid->len); + AD.data[1 + key_uuid->len] = (uint8_t)original_bson_type; + const uint32_t plaintext_len = fle2aead->get_plaintext_len(ciphertext->len, status); +- if (plaintext_len == 0) { ++ if (!mongocrypt_status_ok(status)) { + _mongocrypt_buffer_cleanup(&AD); + return NULL; + } +--- a/src/mongocrypt-crypto.c ++++ b/src/mongocrypt-crypto.c +@@ -445,9 +445,9 @@ + _mongocrypt_hmac_type_t hmac, + mongocrypt_status_t *status) { + const uint32_t hmaclen = (hmac == HMAC_NONE) ? 0 : MONGOCRYPT_HMAC_LEN; +- const uint32_t mincipher = (mode == MODE_CTR) ? 0 : MONGOCRYPT_BLOCK_SIZE; ++ const uint32_t mincipher = (mode == MODE_CTR) ? 1 : MONGOCRYPT_BLOCK_SIZE; + if (inlen < (MONGOCRYPT_IV_LEN + mincipher + hmaclen)) { +- CLIENT_ERR("input ciphertext too small. Must be at least %" PRIu32 " bytes", ++ CLIENT_ERR("input ciphertext too small. Must be more than %" PRIu32 " bytes", + MONGOCRYPT_IV_LEN + mincipher + hmaclen); + return 0; + } +@@ -1340,7 +1340,11 @@ + + // _mongocrypt_wrap_key() uses FLE1 algorithm parameters. + _mongocrypt_buffer_init(dek); +- _mongocrypt_buffer_resize(dek, fle1alg->get_plaintext_len(encrypted_dek->len, status)); ++ const uint32_t plaintext_len = fle1alg->get_plaintext_len(encrypted_dek->len, status); ++ if (!mongocrypt_status_ok(status)) { ++ return false; ++ } ++ _mongocrypt_buffer_resize(dek, plaintext_len); + + if (!fle1alg->do_decrypt(crypto, NULL /* associated data. */, kek, encrypted_dek, dek, &bytes_written, status)) { + return false; +--- a/src/mongocrypt-ctx-decrypt.c ++++ b/src/mongocrypt-ctx-decrypt.c +@@ -341,7 +341,7 @@ + + const _mongocrypt_value_encryption_algorithm_t *fle1alg = _mcFLE1Algorithm(); + plaintext.len = fle1alg->get_plaintext_len(ciphertext.data.len, status); +- CHECK_AND_RETURN(plaintext.len != 0); ++ CHECK_AND_RETURN(mongocrypt_status_ok(status)); + plaintext.data = bson_malloc0(plaintext.len); + BSON_ASSERT(plaintext.data); + +--- a/test/test-mc-fle2-payload-iup-v2.c ++++ b/test/test-mc-fle2-payload-iup-v2.c +@@ -263,10 +263,74 @@ + _mongocrypt_buffer_cleanup(&input_buf); + } + ++/* Builds a minimal parseable FLE2InsertUpdatePayloadV2 with a 'v' of `vlen` bytes. ++ * 'v' is a 16 byte user key UUID followed by (vlen - 16) bytes of ciphertext. */ ++static void _make_iup_v2_with_value_len(_mongocrypt_buffer_t *out, uint32_t vlen) { ++ BSON_ASSERT(vlen >= UUID_LEN); ++ uint8_t token[32] = {0}; ++ uint8_t uuid[UUID_LEN]; ++ uint8_t value[128]; ++ ++ memset(uuid, 0xAB, sizeof(uuid)); ++ memset(value, 0x41, sizeof(value)); ++ BSON_ASSERT(vlen <= sizeof(value)); ++ memcpy(value, uuid, sizeof(uuid)); ++ ++ bson_t payload; ++ bson_init(&payload); ++ ASSERT(bson_append_binary(&payload, "d", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "s", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "p", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "u", -1, BSON_SUBTYPE_UUID, uuid, sizeof(uuid))); ++ ASSERT(bson_append_int32(&payload, "t", -1, (int32_t)BSON_TYPE_UTF8)); ++ ASSERT(bson_append_binary(&payload, "v", -1, BSON_SUBTYPE_BINARY, value, vlen)); ++ ASSERT(bson_append_binary(&payload, "e", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "l", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_int64(&payload, "k", -1, 0)); ++ ++ _mongocrypt_buffer_init_size(out, 1 + (uint32_t)payload.len); ++ out->data[0] = (uint8_t)MC_SUBTYPE_FLE2InsertUpdatePayloadV2; ++ memcpy(out->data + 1, bson_get_data(&payload), payload.len); ++ bson_destroy(&payload); ++} ++ ++/* A 'v' short enough that the plaintext length would be zero must be rejected ++ * with an error, not crash. */ ++static void _test_mc_FLE2InsertUpdatePayloadV2_decrypt_short_ciphertext(_mongocrypt_tester_t *tester) { ++ mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); ++ _mongocrypt_buffer_t user_key; ++ _mongocrypt_buffer_init_size(&user_key, MONGOCRYPT_KEY_LEN); ++ memset(user_key.data, 0xCD, user_key.len); ++ ++ /* FLE2v2AEAD is CBC + HMAC-SHA256: ciphertext must be at least ++ * IV (16) + one block (16) + HMAC (32) bytes. */ ++ const uint32_t vlens[] = {UUID_LEN, UUID_LEN + 1, UUID_LEN + 48, UUID_LEN + 63}; ++ for (size_t i = 0; i < sizeof(vlens) / sizeof(vlens[0]); i++) { ++ _mongocrypt_buffer_t input; ++ _make_iup_v2_with_value_len(&input, vlens[i]); ++ ++ mc_FLE2InsertUpdatePayloadV2_t iup; ++ mc_FLE2InsertUpdatePayloadV2_init(&iup); ++ mongocrypt_status_t *status = mongocrypt_status_new(); ++ ASSERT_OK_STATUS(mc_FLE2InsertUpdatePayloadV2_parse(&iup, &input, status), status); ++ ++ const _mongocrypt_buffer_t *got = mc_FLE2InsertUpdatePayloadV2_decrypt(crypt->crypto, &iup, &user_key, status); ++ ASSERT_FAILS_STATUS(got != NULL, status, "input ciphertext too small"); ++ ++ mc_FLE2InsertUpdatePayloadV2_cleanup(&iup); ++ mongocrypt_status_destroy(status); ++ _mongocrypt_buffer_cleanup(&input); ++ } ++ ++ _mongocrypt_buffer_cleanup(&user_key); ++ mongocrypt_destroy(crypt); ++} ++ + void _mongocrypt_tester_install_fle2_payload_iup_v2(_mongocrypt_tester_t *tester) { + INSTALL_TEST(_test_FLE2InsertUpdatePayloadV2_parse); + INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_decrypt); + INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_includes_crypto_params); + INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_parses_crypto_params); + INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_parse_errors); ++ INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_decrypt_short_ciphertext); + } +--- a/test/test-mc-fle2-payload-iup.c ++++ b/test/test-mc-fle2-payload-iup.c +@@ -182,8 +182,75 @@ + _mongocrypt_buffer_cleanup(&input_buf); + } + ++/* Builds a minimal parseable FLE2InsertUpdatePayload with a 'v' of `vlen` bytes. ++ * 'v' is a 16 byte user key UUID followed by (vlen - 16) bytes of ciphertext. */ ++static void _make_iup_with_value_len(_mongocrypt_buffer_t *out, uint32_t vlen) { ++ BSON_ASSERT(vlen >= UUID_LEN); ++ uint8_t token[32] = {0}; ++ uint8_t uuid[UUID_LEN]; ++ uint8_t value[128]; ++ ++ memset(uuid, 0xAB, sizeof(uuid)); ++ memset(value, 0x41, sizeof(value)); ++ BSON_ASSERT(vlen <= sizeof(value)); ++ memcpy(value, uuid, sizeof(uuid)); ++ ++ bson_t payload; ++ bson_init(&payload); ++ ASSERT(bson_append_binary(&payload, "d", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "s", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "c", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "p", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ASSERT(bson_append_binary(&payload, "u", -1, BSON_SUBTYPE_UUID, uuid, sizeof(uuid))); ++ ASSERT(bson_append_int32(&payload, "t", -1, (int32_t)BSON_TYPE_UTF8)); ++ ASSERT(bson_append_binary(&payload, "v", -1, BSON_SUBTYPE_BINARY, value, vlen)); ++ ASSERT(bson_append_binary(&payload, "e", -1, BSON_SUBTYPE_BINARY, token, sizeof(token))); ++ ++ _mongocrypt_buffer_init_size(out, 1 + (uint32_t)payload.len); ++ out->data[0] = (uint8_t)MC_SUBTYPE_FLE2InsertUpdatePayload; ++ memcpy(out->data + 1, bson_get_data(&payload), payload.len); ++ bson_destroy(&payload); ++} ++ ++/* A 'v' short enough that the plaintext length would be zero must be rejected ++ * with an error, not crash. */ ++static void test_FLE2InsertUpdatePayload_decrypt_short_ciphertext(_mongocrypt_tester_t *tester) { ++ if (!_aes_ctr_is_supported_by_os) { ++ TEST_PRINTF("Common Crypto with no CTR support detected. Skipping."); ++ return; ++ } ++ ++ mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); ++ _mongocrypt_buffer_t user_key; ++ _mongocrypt_buffer_init_size(&user_key, MONGOCRYPT_KEY_LEN); ++ memset(user_key.data, 0xCD, user_key.len); ++ ++ /* FLE2AEAD is CTR + HMAC-SHA256: ciphertext must exceed IV (16) + HMAC (32). */ ++ const uint32_t vlens[] = {UUID_LEN, UUID_LEN + 1, UUID_LEN + 47, UUID_LEN + 48}; ++ for (size_t i = 0; i < sizeof(vlens) / sizeof(vlens[0]); i++) { ++ _mongocrypt_buffer_t input; ++ _make_iup_with_value_len(&input, vlens[i]); ++ ++ mc_FLE2InsertUpdatePayload_t iup; ++ mc_FLE2InsertUpdatePayload_init(&iup); ++ mongocrypt_status_t *status = mongocrypt_status_new(); ++ ASSERT_OK_STATUS(mc_FLE2InsertUpdatePayload_parse(&iup, &input, status), status); ++ ++ const _mongocrypt_buffer_t *got = mc_FLE2InsertUpdatePayload_decrypt(crypt->crypto, &iup, &user_key, status); ++ ASSERT_FAILS_STATUS(got != NULL, status, "input ciphertext too small"); ++ ++ mc_FLE2InsertUpdatePayload_cleanup(&iup); ++ mongocrypt_status_destroy(status); ++ _mongocrypt_buffer_cleanup(&input); ++ } ++ ++ _mongocrypt_buffer_cleanup(&user_key); ++ mongocrypt_destroy(crypt); ++} ++ + void _mongocrypt_tester_install_fle2_payload_iup(_mongocrypt_tester_t *tester) { + INSTALL_TEST(test_FLE2InsertUpdatePayload_parse); + INSTALL_TEST(test_FLE2InsertUpdatePayload_decrypt); + INSTALL_TEST(test_FLE2InsertUpdatePayload_parse_errors); ++ INSTALL_TEST(test_FLE2InsertUpdatePayload_decrypt_short_ciphertext); + } diff -Nru libmongocrypt-1.13.2/debian/patches/0004_CVE-2026-106429_1_of_3.patch libmongocrypt-1.13.2/debian/patches/0004_CVE-2026-106429_1_of_3.patch --- libmongocrypt-1.13.2/debian/patches/0004_CVE-2026-106429_1_of_3.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0004_CVE-2026-106429_1_of_3.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,97 @@ +From f2ea9f97dff40eb8b470a4fc3d407bafb4650d88 Mon Sep 17 00:00:00 2001 +From: Kevin Albertson +Date: Fri, 18 Sep 2026 14:15:22 -0400 +Subject: [PATCH] Fix parsing a colon after path or query + +This fixes a too-big allocation triggered by an endpoint like "example.com/path:8080". Parsing resulted in a negative port length that underflowed a size_t used for allocation. +--- + src/mongocrypt-endpoint.c | 21 +++++++++++---------- + test/test-mongocrypt-endpoint.c | 30 ++++++++++++++++++++++++++++++ + 2 files changed, 41 insertions(+), 10 deletions(-) + +diff --git a/src/mongocrypt-endpoint.c b/src/mongocrypt-endpoint.c +index baf6dc04..44b9eaab 100644 +--- a/src/mongocrypt-endpoint.c ++++ b/src/mongocrypt-endpoint.c +@@ -92,14 +92,15 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw, + colon = strstr(pos, ":"); + qmark = strstr(pos, "?"); + slash = strstr(pos, "/"); +- if (colon) { +- host_end = colon; +- } else if (slash) { ++ /* The host ends at the first delimiter. A colon delimits a port only if it ++ * precedes the path and query: in "example.com/path:8080" the colon is part ++ * of the path, not a port separator. */ ++ host_end = colon; ++ if (slash && (!host_end || slash < host_end)) { + host_end = slash; +- } else if (qmark) { ++ } ++ if (qmark && (!host_end || qmark < host_end)) { + host_end = qmark; +- } else { +- host_end = NULL; + } + + if (host_end) { +@@ -113,11 +114,11 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw, + } + + /* Parse optional port */ +- if (colon) { ++ if (colon && colon == host_end) { + prev = colon + 1; +- qmark = strstr(pos, "?"); +- slash = strstr(pos, "/"); +- if (slash) { ++ qmark = strstr(prev, "?"); ++ slash = strstr(prev, "/"); ++ if (slash && (!qmark || slash < qmark)) { + endpoint->port = bson_strndup(prev, (size_t)(slash - prev)); + } else if (qmark) { + BSON_ASSERT(qmark >= prev); +diff --git a/test/test-mongocrypt-endpoint.c b/test/test-mongocrypt-endpoint.c +index 474a1ec8..51e400c6 100644 +--- a/test/test-mongocrypt-endpoint.c ++++ b/test/test-mongocrypt-endpoint.c +@@ -66,6 +66,36 @@ static void _test_mongocrypt_endpoint(_mongocrypt_tester_t *tester) { + ASSERT_STATUS_CONTAINS(status, "Invalid endpoint, expected dot separator in host, but got: malformed"); + _mongocrypt_endpoint_destroy(endpoint); + ++ /* Test a colon in the path does not parse as a port. */ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com/path:8080", -1, NULL /* opts */, status); ++ BSON_ASSERT(endpoint); ++ ASSERT_STREQUAL(endpoint->host, "vault.example.com"); ++ ASSERT_STREQUAL(endpoint->domain, "example.com"); ++ BSON_ASSERT(!endpoint->port); ++ ASSERT_STREQUAL(endpoint->path, "path:8080"); ++ ASSERT_STREQUAL(endpoint->host_and_port, "vault.example.com"); ++ BSON_ASSERT(mongocrypt_status_ok(status)); ++ _mongocrypt_endpoint_destroy(endpoint); ++ ++ /* Test a colon in the query does not parse as a port. */ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com?query=a:b", -1, NULL /* opts */, status); ++ BSON_ASSERT(endpoint); ++ ASSERT_STREQUAL(endpoint->host, "vault.example.com"); ++ BSON_ASSERT(!endpoint->port); ++ ASSERT_STREQUAL(endpoint->query, "query=a:b"); ++ BSON_ASSERT(mongocrypt_status_ok(status)); ++ _mongocrypt_endpoint_destroy(endpoint); ++ ++ /* Test a port followed by a query, with no path. */ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com:443?query=value", -1, NULL /* opts */, status); ++ BSON_ASSERT(endpoint); ++ ASSERT_STREQUAL(endpoint->host, "vault.example.com"); ++ ASSERT_STREQUAL(endpoint->port, "443"); ++ ASSERT_STREQUAL(endpoint->query, "query=value"); ++ ASSERT_STREQUAL(endpoint->host_and_port, "vault.example.com:443"); ++ BSON_ASSERT(mongocrypt_status_ok(status)); ++ _mongocrypt_endpoint_destroy(endpoint); ++ + /* A host without a dot separator is valid if the "allow_empty_subdomain" + * option is true. */ + memset(&opts, 0, sizeof(opts)); +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0005_CVE-2026-106429_2_of_3.patch libmongocrypt-1.13.2/debian/patches/0005_CVE-2026-106429_2_of_3.patch --- libmongocrypt-1.13.2/debian/patches/0005_CVE-2026-106429_2_of_3.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0005_CVE-2026-106429_2_of_3.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,353 @@ +From 1450714452daaaa9da29359ab6a6b1a0c3a1fa47 Mon Sep 17 00:00:00 2001 +From: Kevin Albertson +Date: Fri, 18 Sep 2026 14:16:50 -0400 +Subject: [PATCH] Reject invalid characters in a KMS endpoint + +This is a defense-in-depth to reject characters outside of those accepted by RFC 3986. + +This change is not intended to address the reported injection bugs. +--- + src/mongocrypt-endpoint-private.h | 1 + + src/mongocrypt-endpoint.c | 111 +++++++++++++++++++++++ + test/data/kek-tests.json | 6 +- + test/test-mongocrypt-endpoint.c | 140 ++++++++++++++++++++++++++++++ + 4 files changed, 255 insertions(+), 3 deletions(-) + +diff --git a/src/mongocrypt-endpoint-private.h b/src/mongocrypt-endpoint-private.h +index 96f01577..56a01877 100644 +--- a/src/mongocrypt-endpoint-private.h ++++ b/src/mongocrypt-endpoint-private.h +@@ -57,4 +57,5 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw, + */ + void _mongocrypt_apply_default_port(char **endpoint_raw, char *port); + ++ + #endif /* MONGOCRYPT_ENDPOINT_PRIVATE_H */ +diff --git a/src/mongocrypt-endpoint.c b/src/mongocrypt-endpoint.c +index 44b9eaab..8ba402ac 100644 +--- a/src/mongocrypt-endpoint.c ++++ b/src/mongocrypt-endpoint.c +@@ -34,6 +34,104 @@ void _mongocrypt_endpoint_destroy(_mongocrypt_endpoint_t *endpoint) { + bson_free(endpoint); + } + ++/* RFC 3986 section 2.3: unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" */ ++static bool _is_unreserved(char c) { ++ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.' ++ || c == '_' || c == '~'; ++} ++ ++/* RFC 3986 section 2.2: reserved = gen-delims / sub-delims */ ++static bool _is_reserved(char c) { ++ return NULL ++ != strchr(":/?#[]@" /* gen-delims */ ++ "!$&'()*+,;=" /* sub-delims */, ++ c); ++} ++ ++static bool _is_hexdig(char c) { ++ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); ++} ++ ++/* _endpoint_chars_are_valid returns true if `endpoint_raw` contains only characters permitted in a URI ++ * by RFC 3986. Useful to prevent malformed endpoints being used to construct HTTP requests. */ ++static bool _endpoint_chars_are_valid(const char *endpoint_raw, mongocrypt_status_t *status) { ++ for (const char *c = endpoint_raw; *c != '\0'; c++) { ++ if (_is_unreserved(*c) || _is_reserved(*c)) { ++ continue; ++ } ++ if (*c == '%') { ++ /* Returns false if either character is the NUL terminator. */ ++ if (!_is_hexdig(c[1])) { ++ CLIENT_ERR("Invalid percent-encoding in endpoint at offset %zu: 0x%02x", ++ (size_t)(c - endpoint_raw) + 1, ++ (unsigned char)c[1]); ++ return false; ++ } ++ if (!_is_hexdig(c[2])) { ++ CLIENT_ERR("Invalid percent-encoding in endpoint at offset %zu: 0x%02x", ++ (size_t)(c - endpoint_raw) + 2, ++ (unsigned char)c[2]); ++ return false; ++ } ++ c += 2; ++ continue; ++ } ++ CLIENT_ERR("Invalid character in endpoint at offset %zu: 0x%02x", ++ (size_t)(c - endpoint_raw), ++ (unsigned char)*c); ++ return false; ++ } ++ return true; ++} ++ ++/* RFC 3986 section 2.2: sub-delims. Permitted in a reg-name; gen-delims are not. */ ++static bool _is_sub_delim(char c) { ++ return NULL != strchr("!$&'()*+,;=", c); ++} ++ ++/* _host_chars_are_valid returns true if `host` is an RFC 3986 section 3.2.2 `reg-name`: ++ * ++ * reg-name = *( unreserved / pct-encoded / sub-delims ) ++ * ++ * The gen-delims (":/?#[]@") are not permitted. The parse above already ends the host at ':', '/', and '?', ++ * so in practice this rejects '#', '@', and the brackets of an IP-literal. ++ * ++ * This is defense in depth. `host` reaches the Host header, where CR and LF are the only characters that can ++ * alter the message and kms-message rejects them, and `host_and_port` is passed to the consumer as the ++ * address to connect to, where a gen-delim yields a name that does not resolve rather than a structural ++ * problem. An `IP-literal` (RFC 3986 section 3.2.2) is not supported: the parse splits the host at the first ++ * colon with no bracket handling, so "[2001:db8::1]" yielded a host of "[2001" and a port of "db8::1]". */ ++static bool _host_chars_are_valid(const char *host, mongocrypt_status_t *status) { ++ for (const char *c = host; *c != '\0'; c++) { ++ if (_is_unreserved(*c) || _is_sub_delim(*c)) { ++ continue; ++ } ++ if (*c == '%') { ++ /* Already validated by _endpoint_chars_are_valid. */ ++ c += 2; ++ continue; ++ } ++ CLIENT_ERR("Invalid character in endpoint host: 0x%02x", (unsigned char)*c); ++ return false; ++ } ++ return true; ++} ++ ++/* _port_chars_are_valid returns true if `port` is a non-empty run of digits. */ ++static bool _port_chars_are_valid(const char *port, mongocrypt_status_t *status) { ++ if (*port == '\0') { ++ CLIENT_ERR("Invalid endpoint, expected a port after the colon"); ++ return false; ++ } ++ for (const char *c = port; *c != '\0'; c++) { ++ if (*c < '0' || *c > '9') { ++ CLIENT_ERR("Invalid character in endpoint port: 0x%02x", (unsigned char)*c); ++ return false; ++ } ++ } ++ return true; ++} ++ + /* Parses a subset of URIs of the form: + * [protocol://][host[:port]][path][?query] + */ +@@ -61,6 +159,11 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw, + goto fail; + } + ++ /* Validate before parsing: every field parsed below is a substring of `original`. */ ++ if (!_endpoint_chars_are_valid(endpoint->original, status)) { ++ goto fail; ++ } ++ + /* Parse optional protocol. */ + pos = strstr(endpoint->original, "://"); + if (pos) { +@@ -113,6 +216,10 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw, + endpoint->host = bson_strdup(host_start); + } + ++ if (!_host_chars_are_valid(endpoint->host, status)) { ++ goto fail; ++ } ++ + /* Parse optional port */ + if (colon && colon == host_end) { + prev = colon + 1; +@@ -126,6 +233,10 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw, + } else { + endpoint->port = bson_strdup(prev); + } ++ ++ if (!_port_chars_are_valid(endpoint->port, status)) { ++ goto fail; ++ } + } + + /* Parse optional path */ +diff --git a/test/data/kek-tests.json b/test/data/kek-tests.json +index 1599c165..8629fb8e 100644 +--- a/test/data/kek-tests.json ++++ b/test/data/kek-tests.json +@@ -15,7 +15,7 @@ + "keyName": "example keyName", + "provider": "azure" + }, +- "expect": "invalid endpoint" ++ "expect": "Invalid character in endpoint" + }, + { + "input": { +@@ -47,7 +47,7 @@ + "key": "example arn", + "provider": "aws" + }, +- "expect": "invalid endpoint" ++ "expect": "Invalid character in endpoint" + }, + { + "input": { +@@ -79,7 +79,7 @@ + "keyRing": "example keyRing", + "keyName": "example keyName" + }, +- "expect": "invalid endpoint" ++ "expect": "Invalid character in endpoint" + }, + { + "input": { +diff --git a/test/test-mongocrypt-endpoint.c b/test/test-mongocrypt-endpoint.c +index 51e400c6..d71e1faa 100644 +--- a/test/test-mongocrypt-endpoint.c ++++ b/test/test-mongocrypt-endpoint.c +@@ -96,6 +96,146 @@ static void _test_mongocrypt_endpoint(_mongocrypt_tester_t *tester) { + BSON_ASSERT(mongocrypt_status_ok(status)); + _mongocrypt_endpoint_destroy(endpoint); + ++ /* An endpoint may contain only the characters RFC 3986 permits in a URI. This is defense in depth: ++ * each context an endpoint reaches is validated where it is used. */ ++ const char *const invalid[] = { ++ "a\r\nX-Injected: injected.example.com", /* forges a header */ ++ "a\r\n\r\nGET /evil.example.com", /* ends the header section */ ++ "example.com\", \"sub\": \"victim@example.com", /* injects a JWT claim */ ++ "example.com\\x.example.com", /* backslash */ ++ "example .com", /* space */ ++ "example.com\x7f", /* DEL */ ++ "exampl\xc3\xa9.com", /* non-ASCII */ ++ }; ++ for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { ++ endpoint = _mongocrypt_endpoint_new(invalid[i], -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint"); ++ } ++ ++ /* The offending byte is reported by offset and hex value. The endpoint itself is not echoed: it is ++ * not yet validated here, and may contain a CR or LF that would forge a line in a consumer's log. */ ++ endpoint = _mongocrypt_endpoint_new("example .com", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint at offset 7: 0x20"); ++ ++ endpoint = _mongocrypt_endpoint_new("a\r\nX-Injected: injected.example.com", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint at offset 1: 0x0d"); ++ ++ /* An IPv6 literal is rejected. The parse has no bracket handling, so such an endpoint was never ++ * usable. Without a dot it is rejected earlier still, for ++ * lacking a subdomain separator. */ ++ memset(&opts, 0, sizeof(opts)); ++ opts.allow_empty_subdomain = true; ++ endpoint = _mongocrypt_endpoint_new("[2001:db8::1]:443", -1, &opts, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host"); ++ ++ /* One with an embedded dot reaches the host check without the option. */ ++ endpoint = _mongocrypt_endpoint_new("[::ffff:192.0.2.1]", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host"); ++ ++ /* An IPv4 literal is accepted, as before. */ ++ endpoint = _mongocrypt_endpoint_new("192.0.2.1:443", -1, NULL /* opts */, status); ++ BSON_ASSERT(endpoint); ++ ASSERT_STREQUAL(endpoint->host, "192.0.2.1"); ++ ASSERT_STREQUAL(endpoint->port, "443"); ++ ASSERT_STREQUAL(endpoint->host_and_port, "192.0.2.1:443"); ++ BSON_ASSERT(mongocrypt_status_ok(status)); ++ _mongocrypt_endpoint_destroy(endpoint); ++ ++ /* Percent-encoding must be well formed. */ ++ endpoint = _mongocrypt_endpoint_new("example.com/%zz", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid percent-encoding in endpoint at offset 13: 0x7a"); ++ ++ /* A '%' at the end of the string: the second hex digit is the NUL terminator. */ ++ endpoint = _mongocrypt_endpoint_new("example.com/%2", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid percent-encoding in endpoint at offset 14: 0x00"); ++ ++ endpoint = _mongocrypt_endpoint_new("example.com/a%2Fb", -1, NULL /* opts */, status); ++ BSON_ASSERT(endpoint); ++ ASSERT_STREQUAL(endpoint->path, "a%2Fb"); ++ BSON_ASSERT(mongocrypt_status_ok(status)); ++ _mongocrypt_endpoint_destroy(endpoint); ++ ++ /* A host must be an RFC 3986 `reg-name`. The gen-delims are rejected; the parse already ends the host ++ * at ':', '/', and '?', so this covers '#', '@', and the brackets of an IP-literal. */ ++ const char *const invalid_hosts[] = { ++ "vault.example.com@evil.example.com", /* userinfo separator */ ++ "vault.example.com#frag", ++ "[2001:db8::1]", ++ }; ++ memset(&opts, 0, sizeof(opts)); ++ opts.allow_empty_subdomain = true; ++ for (size_t i = 0; i < sizeof(invalid_hosts) / sizeof(invalid_hosts[0]); i++) { ++ endpoint = _mongocrypt_endpoint_new(invalid_hosts[i], -1, &opts, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host"); ++ } ++ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com@evil.example.com", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host: 0x40"); ++ ++ /* The sub-delims are permitted in a reg-name, so they are permitted here. An '&' reaching the Azure ++ * OAuth request body is prevented by percent-encoding it into the scope, not by this check. */ ++ const char *const valid_hosts[] = { ++ "vault.example.com&x", ++ "vault.example.com=x", ++ "vault.example.com;x", ++ "vault.example.com,x", ++ "vault.example.com~x", ++ "vault.example.com%41", ++ }; ++ for (size_t i = 0; i < sizeof(valid_hosts) / sizeof(valid_hosts[0]); i++) { ++ endpoint = _mongocrypt_endpoint_new(valid_hosts[i], -1, NULL /* opts */, status); ++ BSON_ASSERT(endpoint); ++ ASSERT_STREQUAL(endpoint->host, valid_hosts[i]); ++ BSON_ASSERT(mongocrypt_status_ok(status)); ++ _mongocrypt_endpoint_destroy(endpoint); ++ } ++ ++ /* The host check does not apply to the path or query, which may contain reserved characters. */ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com/p&q?a=b&c=d", -1, NULL /* opts */, status); ++ BSON_ASSERT(endpoint); ++ ASSERT_STREQUAL(endpoint->host, "vault.example.com"); ++ ASSERT_STREQUAL(endpoint->path, "p&q"); ++ ASSERT_STREQUAL(endpoint->query, "a=b&c=d"); ++ BSON_ASSERT(mongocrypt_status_ok(status)); ++ _mongocrypt_endpoint_destroy(endpoint); ++ ++ /* A port may contain only digits. The port is appended to host_and_port, which is used as the Host ++ * header and as the address to connect to. */ ++ const char *const invalid_ports[] = { ++ "vault.example.com:44&3", ++ "vault.example.com:443x", ++ "vault.example.com:-1", ++ "vault.example.com:44.3", ++ }; ++ for (size_t i = 0; i < sizeof(invalid_ports) / sizeof(invalid_ports[0]); i++) { ++ endpoint = _mongocrypt_endpoint_new(invalid_ports[i], -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint port"); ++ } ++ ++ /* The offset is relative to the start of the port. */ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com:44&3", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint port: 0x26"); ++ ++ /* A colon with no port is rejected. */ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com:", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid endpoint, expected a port after the colon"); ++ ++ endpoint = _mongocrypt_endpoint_new("vault.example.com:/path", -1, NULL /* opts */, status); ++ BSON_ASSERT(!endpoint); ++ ASSERT_STATUS_CONTAINS(status, "Invalid endpoint, expected a port after the colon"); ++ + /* A host without a dot separator is valid if the "allow_empty_subdomain" + * option is true. */ + memset(&opts, 0, sizeof(opts)); +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0006_CVE-2026-106429_3_of_3.patch libmongocrypt-1.13.2/debian/patches/0006_CVE-2026-106429_3_of_3.patch --- libmongocrypt-1.13.2/debian/patches/0006_CVE-2026-106429_3_of_3.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0006_CVE-2026-106429_3_of_3.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,22 @@ +From 03d09f07bb32f9387aec823444cd7bb6d41cddad Mon Sep 17 00:00:00 2001 +From: Kevin Albertson +Date: Tue, 29 Sep 2026 16:05:24 -0400 +Subject: [PATCH] Remove whitespace change + +--- + src/mongocrypt-endpoint-private.h | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/src/mongocrypt-endpoint-private.h b/src/mongocrypt-endpoint-private.h +index 56a01877..96f01577 100644 +--- a/src/mongocrypt-endpoint-private.h ++++ b/src/mongocrypt-endpoint-private.h +@@ -57,5 +57,4 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw, + */ + void _mongocrypt_apply_default_port(char **endpoint_raw, char *port); + +- + #endif /* MONGOCRYPT_ENDPOINT_PRIVATE_H */ +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0007_CVE-2026-106434_1_of_2.patch libmongocrypt-1.13.2/debian/patches/0007_CVE-2026-106434_1_of_2.patch --- libmongocrypt-1.13.2/debian/patches/0007_CVE-2026-106434_1_of_2.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0007_CVE-2026-106434_1_of_2.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,111 @@ +From 5e68bbf5825b59978e21c595c1af6d39a5e00e94 Mon Sep 17 00:00:00 2001 +From: Connor MacDonald +Date: Mon, 28 Sep 2026 10:29:12 -0400 +Subject: [PATCH] Fix handling of unknown FLE blob subtype + +--- + src/mongocrypt-ctx-decrypt.c | 34 ++++++++++++++++++++++++++++++ + test/test-mongocrypt-ctx-decrypt.c | 28 ++++++++++++++++++++++++ + 2 files changed, 62 insertions(+) + +diff --git a/src/mongocrypt-ctx-decrypt.c b/src/mongocrypt-ctx-decrypt.c +index 5fe50c14..05b14e09 100644 +--- a/src/mongocrypt-ctx-decrypt.c ++++ b/src/mongocrypt-ctx-decrypt.c +@@ -766,6 +766,32 @@ static void _cleanup(mongocrypt_ctx_t *ctx) { + _mongocrypt_buffer_cleanup(&dctx->decrypted_doc); + } + ++/* Returns true if a subtype-6 payload's first byte names a recognized FLE blob ++ * subtype. */ ++static bool _is_fle_blob_subtype(uint8_t byte) { ++ switch (byte) { ++ case MC_SUBTYPE_FLE1EncryptionPlaceholder: ++ case MC_SUBTYPE_FLE1DeterministicEncryptedValue: ++ case MC_SUBTYPE_FLE1RandomEncryptedValue: ++ case MC_SUBTYPE_FLE2EncryptionPlaceholder: ++ case MC_SUBTYPE_FLE2InsertUpdatePayload: ++ case MC_SUBTYPE_FLE2FindEqualityPayload: ++ case MC_SUBTYPE_FLE2UnindexedEncryptedValue: ++ case MC_SUBTYPE_FLE2IndexedEqualityEncryptedValue: ++ case MC_SUBTYPE_FLE2IndexedRangeEncryptedValue: ++ case MC_SUBTYPE_FLE2FindRangePayload: ++ case MC_SUBTYPE_FLE2InsertUpdatePayloadV2: ++ case MC_SUBTYPE_FLE2FindEqualityPayloadV2: ++ case MC_SUBTYPE_FLE2FindRangePayloadV2: ++ case MC_SUBTYPE_FLE2IndexedEqualityEncryptedValueV2: ++ case MC_SUBTYPE_FLE2IndexedRangeEncryptedValueV2: ++ case MC_SUBTYPE_FLE2UnindexedEncryptedValueV2: ++ case MC_SUBTYPE_FLE2IndexedTextEncryptedValue: ++ case MC_SUBTYPE_FLE2FindTextPayload: return true; ++ default: return false; ++ } ++} ++ + bool mongocrypt_ctx_explicit_decrypt_init(mongocrypt_ctx_t *ctx, mongocrypt_binary_t *msg) { + bson_iter_t iter; + bson_t as_bson; +@@ -804,6 +830,14 @@ bool mongocrypt_ctx_explicit_decrypt_init(mongocrypt_ctx_t *ctx, mongocrypt_bina + CLIENT_ERR("decryption expected BSON binary subtype %d, got %d", (int)BSON_SUBTYPE_ENCRYPTED, (int)subtype); + return _mongocrypt_ctx_fail(ctx); + } ++ if (binary_len == 0) { ++ CLIENT_ERR("unsupported FLE blob subtype: expected non-empty payload"); ++ return _mongocrypt_ctx_fail(ctx); ++ } ++ if (!_is_fle_blob_subtype(binary[0])) { ++ CLIENT_ERR("unsupported FLE blob subtype: %hhu", binary[0]); ++ return _mongocrypt_ctx_fail(ctx); ++ } + } + + if (!mongocrypt_ctx_decrypt_init(ctx, msg)) { +diff --git a/test/test-mongocrypt-ctx-decrypt.c b/test/test-mongocrypt-ctx-decrypt.c +index 826cec64..0ab3d9fc 100644 +--- a/test/test-mongocrypt-ctx-decrypt.c ++++ b/test/test-mongocrypt-ctx-decrypt.c +@@ -46,6 +46,33 @@ static void _test_explicit_decrypt_init(_mongocrypt_tester_t *tester) { + mongocrypt_destroy(crypt); + } + ++/* Regression test for MONGOCRYPT-966 */ ++static void _test_explicit_decrypt_unknown_subtype(_mongocrypt_tester_t *tester) { ++ mongocrypt_t *const crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); ++ ++ /* First byte 0x08 is not a known FLE blob subtype. */ ++ { ++ mongocrypt_binary_t *const msg = TEST_BSON("{ 'v': { '$binary': { 'subType': '06', 'base64': " ++ "'CKq7zA==' } } }"); ++ ++ mongocrypt_ctx_t *const ctx = mongocrypt_ctx_new(crypt); ++ ASSERT_FAILS(mongocrypt_ctx_explicit_decrypt_init(ctx, msg), ctx, "unsupported FLE blob subtype"); ++ BSON_ASSERT(mongocrypt_ctx_state(ctx) == MONGOCRYPT_CTX_ERROR); ++ mongocrypt_ctx_destroy(ctx); ++ } ++ ++ /* A zero-length payload has no FLE blob subtype at all. */ ++ { ++ mongocrypt_binary_t *const msg = TEST_BSON("{ 'v': { '$binary': { 'subType': '06', 'base64': '' } } }"); ++ mongocrypt_ctx_t *const ctx = mongocrypt_ctx_new(crypt); ++ ASSERT_FAILS(mongocrypt_ctx_explicit_decrypt_init(ctx, msg), ctx, "unsupported FLE blob subtype"); ++ BSON_ASSERT(mongocrypt_ctx_state(ctx) == MONGOCRYPT_CTX_ERROR); ++ mongocrypt_ctx_destroy(ctx); ++ } ++ ++ mongocrypt_destroy(crypt); ++} ++ + /* Test individual ctx states. */ + static void _test_decrypt_init(_mongocrypt_tester_t *tester) { + mongocrypt_t *crypt; +@@ -1042,6 +1069,7 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) { + + void _mongocrypt_tester_install_ctx_decrypt(_mongocrypt_tester_t *tester) { + INSTALL_TEST(_test_explicit_decrypt_init); ++ INSTALL_TEST(_test_explicit_decrypt_unknown_subtype); + INSTALL_TEST(_test_decrypt_init); + INSTALL_TEST(_test_decrypt_need_keys); + INSTALL_TEST(_test_decrypt_ready); +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0008_CVE-2026-106434_2_of_2.patch libmongocrypt-1.13.2/debian/patches/0008_CVE-2026-106434_2_of_2.patch --- libmongocrypt-1.13.2/debian/patches/0008_CVE-2026-106434_2_of_2.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0008_CVE-2026-106434_2_of_2.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,194 @@ +From e9d07e8a7d593dde93681943b7070634aebd1980 Mon Sep 17 00:00:00 2001 +From: Connor MacDonald +Date: Mon, 5 Oct 2026 10:25:56 -0400 +Subject: [PATCH] Restrict subtype check to decryptable subtypes + +--- + src/mongocrypt-ctx-decrypt.c | 13 ++----- + test/test-mongocrypt-ctx-decrypt.c | 62 +++++++++++++++--------------- + 2 files changed, 33 insertions(+), 42 deletions(-) + +diff --git a/src/mongocrypt-ctx-decrypt.c b/src/mongocrypt-ctx-decrypt.c +index 05b14e09..00e3aa0f 100644 +--- a/src/mongocrypt-ctx-decrypt.c ++++ b/src/mongocrypt-ctx-decrypt.c +@@ -768,26 +768,19 @@ static void _cleanup(mongocrypt_ctx_t *ctx) { + + /* Returns true if a subtype-6 payload's first byte names a recognized FLE blob + * subtype. */ +-static bool _is_fle_blob_subtype(uint8_t byte) { ++static bool _is_decryptable_fle_blob_subtype(uint8_t byte) { + switch (byte) { +- case MC_SUBTYPE_FLE1EncryptionPlaceholder: + case MC_SUBTYPE_FLE1DeterministicEncryptedValue: + case MC_SUBTYPE_FLE1RandomEncryptedValue: +- case MC_SUBTYPE_FLE2EncryptionPlaceholder: + case MC_SUBTYPE_FLE2InsertUpdatePayload: +- case MC_SUBTYPE_FLE2FindEqualityPayload: + case MC_SUBTYPE_FLE2UnindexedEncryptedValue: + case MC_SUBTYPE_FLE2IndexedEqualityEncryptedValue: + case MC_SUBTYPE_FLE2IndexedRangeEncryptedValue: +- case MC_SUBTYPE_FLE2FindRangePayload: + case MC_SUBTYPE_FLE2InsertUpdatePayloadV2: +- case MC_SUBTYPE_FLE2FindEqualityPayloadV2: +- case MC_SUBTYPE_FLE2FindRangePayloadV2: + case MC_SUBTYPE_FLE2IndexedEqualityEncryptedValueV2: + case MC_SUBTYPE_FLE2IndexedRangeEncryptedValueV2: + case MC_SUBTYPE_FLE2UnindexedEncryptedValueV2: +- case MC_SUBTYPE_FLE2IndexedTextEncryptedValue: +- case MC_SUBTYPE_FLE2FindTextPayload: return true; ++ case MC_SUBTYPE_FLE2IndexedTextEncryptedValue: return true; + default: return false; + } + } +@@ -834,7 +827,7 @@ bool mongocrypt_ctx_explicit_decrypt_init(mongocrypt_ctx_t *ctx, mongocrypt_bina + CLIENT_ERR("unsupported FLE blob subtype: expected non-empty payload"); + return _mongocrypt_ctx_fail(ctx); + } +- if (!_is_fle_blob_subtype(binary[0])) { ++ if (!_is_decryptable_fle_blob_subtype(binary[0])) { + CLIENT_ERR("unsupported FLE blob subtype: %hhu", binary[0]); + return _mongocrypt_ctx_fail(ctx); + } +diff --git a/test/test-mongocrypt-ctx-decrypt.c b/test/test-mongocrypt-ctx-decrypt.c +index 0ab3d9fc..f30e8757 100644 +--- a/test/test-mongocrypt-ctx-decrypt.c ++++ b/test/test-mongocrypt-ctx-decrypt.c +@@ -50,23 +50,28 @@ static void _test_explicit_decrypt_init(_mongocrypt_tester_t *tester) { + static void _test_explicit_decrypt_unknown_subtype(_mongocrypt_tester_t *tester) { + mongocrypt_t *const crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); + +- /* First byte 0x08 is not a known FLE blob subtype. */ +- { +- mongocrypt_binary_t *const msg = TEST_BSON("{ 'v': { '$binary': { 'subType': '06', 'base64': " +- "'CKq7zA==' } } }"); ++ const struct { ++ const char *desc; ++ const char *base64; ++ } cases[] = { ++ {"unrecognized subtype 0x08", "CKq7zA=="}, ++ {"FLE1EncryptionPlaceholder (0x00)", "AA=="}, ++ {"FLE2EncryptionPlaceholder (0x03)", "Aw=="}, ++ {"FLE2FindEqualityPayload (0x05)", "BQ=="}, ++ {"FLE2FindRangePayload (0x0a)", "Cg=="}, ++ {"FLE2FindTextPayload (0x12)", "Eg=="}, ++ {"zero-length payload", ""}, ++ }; ++ ++ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { ++ mongocrypt_binary_t *const msg = ++ TEST_BSON("{ 'v': { '$binary': { 'subType': '06', 'base64': '%s' } } }", cases[i].base64); + + mongocrypt_ctx_t *const ctx = mongocrypt_ctx_new(crypt); +- ASSERT_FAILS(mongocrypt_ctx_explicit_decrypt_init(ctx, msg), ctx, "unsupported FLE blob subtype"); +- BSON_ASSERT(mongocrypt_ctx_state(ctx) == MONGOCRYPT_CTX_ERROR); +- mongocrypt_ctx_destroy(ctx); +- } + +- /* A zero-length payload has no FLE blob subtype at all. */ +- { +- mongocrypt_binary_t *const msg = TEST_BSON("{ 'v': { '$binary': { 'subType': '06', 'base64': '' } } }"); +- mongocrypt_ctx_t *const ctx = mongocrypt_ctx_new(crypt); + ASSERT_FAILS(mongocrypt_ctx_explicit_decrypt_init(ctx, msg), ctx, "unsupported FLE blob subtype"); + BSON_ASSERT(mongocrypt_ctx_state(ctx) == MONGOCRYPT_CTX_ERROR); ++ + mongocrypt_ctx_destroy(ctx); + } + +@@ -815,28 +820,21 @@ typedef struct { + mongocrypt_binary_t *msg; + mongocrypt_binary_t *keys_to_feed[3]; // NULL terminated list. + mongocrypt_binary_t *expect; +- bool expect_ignored; ++ bool expect_error; + } ed_testcase; + + static void ed_testcase_run(ed_testcase *tc) { + printf(" explicit decrypt test: %s ... begin\n", tc->desc); + mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); +- ASSERT_OK(mongocrypt_ctx_explicit_decrypt_init(ctx, tc->msg), ctx); + +- if (tc->expect_ignored) { +- ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_READY); +- { +- mongocrypt_binary_t *got = mongocrypt_binary_new(); +- bool ret = mongocrypt_ctx_finalize(ctx, got); +- ASSERT_OK(ret, ctx); +- // Expect input is returned unchanged. +- ASSERT_MONGOCRYPT_BINARY_EQUAL_BSON(tc->msg, got); +- mongocrypt_binary_destroy(got); +- } ++ if (tc->expect_error) { ++ ASSERT_FAILS(mongocrypt_ctx_explicit_decrypt_init(ctx, tc->msg), ctx, "unsupported FLE blob subtype"); ++ ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_ERROR); + goto cleanup; + } + ++ ASSERT_OK(mongocrypt_ctx_explicit_decrypt_init(ctx, tc->msg), ctx); + ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_MONGO_KEYS); + { + for (size_t i = 0; i < sizeof(tc->keys_to_feed) / sizeof(tc->keys_to_feed[0]); i++) { +@@ -885,12 +883,12 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) { + ed_testcase_run(&tc); + } + +- // FLE1EncryptionPlaceholder is ignored. ++ // FLE1EncryptionPlaceholder is recognized but not decryptable: explicit decryption must error. + // Payload is returned by query analysis and consumed by libmongocrypt. Payload is not expected to be given to user. + { + ed_testcase tc = {.desc = "FLE1EncryptionPlaceholder", + .msg = TEST_FILE("./test/data/explicit-decrypt/FLE1EncryptionPlaceholder.json"), +- .expect_ignored = true}; ++ .expect_error = true}; + ed_testcase_run(&tc); + } + +@@ -905,12 +903,12 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) { + ed_testcase_run(&tc); + } + +- // FLE2EncryptionPlaceholder is ignored. ++ // FLE2EncryptionPlaceholder is recognized but not decryptable: explicit decryption must error. + // Payload is returned by query analysis and consumed by libmongocrypt. Payload is not expected to be given to user. + { + ed_testcase tc = {.desc = "FLE2EncryptionPlaceholder", + .msg = TEST_FILE("./test/data/explicit-decrypt/FLE2EncryptionPlaceholder.json"), +- .expect_ignored = true}; ++ .expect_error = true}; + ed_testcase_run(&tc); + } + +@@ -979,12 +977,12 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) { + ed_testcase_run(&tc); + } + +- // FLE2FindEqualityPayload is ignored. ++ // FLE2FindEqualityPayload is recognized but not decryptable: explicit decryption must error. + // Payload does not contain encrypted ciphertext (only lookup tokens). + { + ed_testcase tc = {.desc = "FLE2FindEqualityPayload", + .msg = TEST_FILE("./test/data/explicit-decrypt/FLE2FindEqualityPayload.json"), +- .expect_ignored = true}; ++ .expect_error = true}; + ed_testcase_run(&tc); + } + +@@ -1057,12 +1055,12 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) { + ed_testcase_run(&tc); + } + +- // FLE2FindEqualityPayloadV2 is ignored. ++ // FLE2FindEqualityPayloadV2 is recognized but not decryptable: explicit decryption must error. + // Payload does not contain encrypted ciphertext (only lookup tokens). + { + ed_testcase tc = {.desc = "FLE2FindEqualityPayloadV2", + .msg = TEST_FILE("./test/data/explicit-decrypt/FLE2FindEqualityPayloadV2.json"), +- .expect_ignored = true}; ++ .expect_error = true}; + ed_testcase_run(&tc); + } + } +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0009_CVE-2026-106433_1_of_2.patch libmongocrypt-1.13.2/debian/patches/0009_CVE-2026-106433_1_of_2.patch --- libmongocrypt-1.13.2/debian/patches/0009_CVE-2026-106433_1_of_2.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0009_CVE-2026-106433_1_of_2.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,71 @@ +From 556bcb70b6efecb7f568d824b03469df3a3bc91a Mon Sep 17 00:00:00 2001 +From: Connor MacDonald +Date: Thu, 27 Aug 2026 11:41:18 -0400 +Subject: [PATCH] Fix duplicate master key reparse + +--- + src/mongocrypt-key.c | 4 ++++ + test/test-mongocrypt-key.c | 26 ++++++++++++++++++++++++++ + 2 files changed, 30 insertions(+) + +diff --git a/src/mongocrypt-key.c b/src/mongocrypt-key.c +index 003ee6d2..47adf7d9 100644 +--- a/src/mongocrypt-key.c ++++ b/src/mongocrypt-key.c +@@ -181,6 +181,10 @@ bool _mongocrypt_key_parse_owned(const bson_t *bson, _mongocrypt_key_doc_t *out, + } + + if (0 == strcmp("masterKey", field)) { ++ if (has_master_key) { ++ CLIENT_ERR("duplicate field 'masterKey' in key document"); ++ return false; ++ } + has_master_key = true; + if (!_parse_masterkey(&iter, out, status)) { + return false; +diff --git a/test/test-mongocrypt-key.c b/test/test-mongocrypt-key.c +index d3f88847..375f15b4 100644 +--- a/test/test-mongocrypt-key.c ++++ b/test/test-mongocrypt-key.c +@@ -182,6 +182,31 @@ static void test_mongocrypt_key_parsing(_mongocrypt_tester_t *tester) { + bson_destroy(&key_bson); + } + ++/* Regression test for MONGOCRYPT-964. */ ++static void test_mongocrypt_key_parsing_duplicate_master_key(_mongocrypt_tester_t *tester) { ++ bson_t key_bson = BSON_INITIALIZER; ++ mongocrypt_status_t *const status = mongocrypt_status_new(); ++ ++ /* Two identical masterKey fields. */ ++ _recreate_and_reset(tester, &key_bson, status, "masterKey", NULL); ++ bson_concat(&key_bson, TMP_BSON("{'masterKey': { 'provider': 'local' }}")); ++ bson_concat(&key_bson, TMP_BSON("{'masterKey': { 'provider': 'local' }}")); ++ _parse_fails(&key_bson, status, "duplicate field 'masterKey'"); ++ ++ /* A GCP masterKey followed by an AWS masterKey missing 'region'. The ++ * aborted second parse leaves the GCP 'keyRing' string in the union slot ++ * shared with aws.endpoint, which cleanup then frees as an endpoint. */ ++ _recreate_and_reset(tester, &key_bson, status, "masterKey", NULL); ++ bson_concat(&key_bson, ++ TMP_BSON("{'masterKey': { 'provider': 'gcp', 'projectId': 'project', 'location': " ++ "'global', 'keyRing': 'ring', 'keyName': 'name' }}")); ++ bson_concat(&key_bson, TMP_BSON("{'masterKey': { 'provider': 'aws', 'key': 'cmk-string' }}")); ++ _parse_fails(&key_bson, status, "duplicate field 'masterKey'"); ++ ++ mongocrypt_status_destroy(status); ++ bson_destroy(&key_bson); ++} ++ + static void test_mongocrypt_key_alt_name_from_iter(_mongocrypt_tester_t *tester) { + mongocrypt_status_t *status; + bson_iter_t iter; +@@ -230,5 +255,6 @@ static void test_mongocrypt_key_alt_name_from_iter(_mongocrypt_tester_t *tester) + + void _mongocrypt_tester_install_key(_mongocrypt_tester_t *tester) { + INSTALL_TEST(test_mongocrypt_key_parsing); ++ INSTALL_TEST(test_mongocrypt_key_parsing_duplicate_master_key); + INSTALL_TEST(test_mongocrypt_key_alt_name_from_iter); + } +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0010_CVE-2026-106433_2_of_2.patch libmongocrypt-1.13.2/debian/patches/0010_CVE-2026-106433_2_of_2.patch --- libmongocrypt-1.13.2/debian/patches/0010_CVE-2026-106433_2_of_2.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0010_CVE-2026-106433_2_of_2.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,138 @@ +From 8bc580ea69eea13a932197221edf72830fbc38a1 Mon Sep 17 00:00:00 2001 +From: Connor MacDonald +Date: Mon, 31 Aug 2026 10:02:31 -0400 +Subject: [PATCH] Fix invalid state from KEK parse failure + +--- + src/mongocrypt-kek.c | 26 +++++++++++++++++++------- + test/test-mongocrypt-kek.c | 36 ++++++++++++++++++++++++++++++++++++ + 2 files changed, 55 insertions(+), 7 deletions(-) + +diff --git a/src/mongocrypt-kek.c b/src/mongocrypt-kek.c +index 9329200e..0dd3d21d 100644 +--- a/src/mongocrypt-kek.c ++++ b/src/mongocrypt-kek.c +@@ -171,18 +171,25 @@ bool _mongocrypt_kek_parse_owned(const bson_t *bson, _mongocrypt_kek_t *kek, mon + BSON_ASSERT_PARAM(bson); + BSON_ASSERT_PARAM(kek); + ++ /* Parse into a scratch KEK and only commit to `kek` once the whole ++ * definition is valid. Writing the provider tag before the union payload is ++ * populated would leave `kek` in a state where the tag and the union ++ * disagree, which _mongocrypt_kek_cleanup would act on. */ ++ _mongocrypt_kek_t parsed; ++ memset(&parsed, 0, sizeof(parsed)); ++ + if (!_mongocrypt_parse_required_utf8(bson, "provider", &kms_provider, status)) { + goto done; + } + +- kek->kmsid = bson_strdup(kms_provider); ++ parsed.kmsid = bson_strdup(kms_provider); + + _mongocrypt_kms_provider_t type; +- if (!mc_kmsid_parse(kek->kmsid, &type, &kek->kmsid_name, status)) { ++ if (!mc_kmsid_parse(parsed.kmsid, &type, &parsed.kmsid_name, status)) { + goto done; + } + +- kek->kms_provider = type; ++ parsed.kms_provider = type; + switch (type) { + default: + case MONGOCRYPT_KMS_PROVIDER_NONE: { +@@ -190,7 +197,7 @@ bool _mongocrypt_kek_parse_owned(const bson_t *bson, _mongocrypt_kek_t *kek, mon + goto done; + } + case MONGOCRYPT_KMS_PROVIDER_AWS: { +- if (!_mongocrypt_aws_kek_parse(&kek->provider.aws, kek->kmsid, bson, status)) { ++ if (!_mongocrypt_aws_kek_parse(&parsed.provider.aws, parsed.kmsid, bson, status)) { + goto done; + } + break; +@@ -202,27 +209,32 @@ bool _mongocrypt_kek_parse_owned(const bson_t *bson, _mongocrypt_kek_t *kek, mon + break; + } + case MONGOCRYPT_KMS_PROVIDER_AZURE: { +- if (!_mongocrypt_azure_kek_parse(&kek->provider.azure, kek->kmsid, bson, status)) { ++ if (!_mongocrypt_azure_kek_parse(&parsed.provider.azure, parsed.kmsid, bson, status)) { + goto done; + } + break; + } + case MONGOCRYPT_KMS_PROVIDER_GCP: { +- if (!_mongocrypt_gcp_kek_parse(&kek->provider.gcp, kek->kmsid, bson, status)) { ++ if (!_mongocrypt_gcp_kek_parse(&parsed.provider.gcp, parsed.kmsid, bson, status)) { + goto done; + } + break; + } + case MONGOCRYPT_KMS_PROVIDER_KMIP: { +- if (!_mongocrypt_kmip_kek_parse(&kek->provider.kmip, kek->kmsid, bson, status)) { ++ if (!_mongocrypt_kmip_kek_parse(&parsed.provider.kmip, parsed.kmsid, bson, status)) { + goto done; + } + break; + } + } + ++ /* Success: replace any prior contents of `kek` and take ownership. */ ++ _mongocrypt_kek_cleanup(kek); ++ *kek = parsed; ++ memset(&parsed, 0, sizeof(parsed)); + ret = true; + done: ++ _mongocrypt_kek_cleanup(&parsed); + bson_free(kms_provider); + return ret; + } +diff --git a/test/test-mongocrypt-kek.c b/test/test-mongocrypt-kek.c +index f4da6bb2..c948621a 100644 +--- a/test/test-mongocrypt-kek.c ++++ b/test/test-mongocrypt-kek.c +@@ -96,6 +96,42 @@ static void test_mongocrypt_kek_parsing(_mongocrypt_tester_t *tester) { + bson_destroy(&test_file); + } + ++/* A failed parse must leave the KEK exactly as it was. Committing the new ++ * provider tag before the provider-specific parse repopulates the union would ++ * leave the previous provider's pointers readable under the new tag, which ++ * _mongocrypt_kek_cleanup would then free as the wrong type. See ++ * MONGOCRYPT-964. */ ++static void test_mongocrypt_kek_parse_failure_leaves_kek_intact(_mongocrypt_tester_t *tester) { ++ mongocrypt_status_t *const status = mongocrypt_status_new(); ++ _mongocrypt_kek_t kek; ++ ++ memset(&kek, 0, sizeof(_mongocrypt_kek_t)); ++ ++ ASSERT_OK_STATUS(_mongocrypt_kek_parse_owned(TMP_BSON("{'provider': 'gcp', 'projectId': 'project', " ++ "'location': 'global', 'keyRing': 'ring', " ++ "'keyName': 'name'}"), ++ &kek, ++ status), ++ status); ++ ++ /* An AWS definition missing the required 'region' aborts mid-parse, after ++ * the provider tag would have been switched. */ ++ ASSERT_FAILS_STATUS(_mongocrypt_kek_parse_owned(TMP_BSON("{'provider': 'aws', 'key': 'cmk-string'}"), &kek, status), ++ status, ++ "expected UTF-8 region"); ++ ++ /* The KEK still describes the GCP key it was parsed from. */ ++ ASSERT_CMPINT(kek.kms_provider, ==, MONGOCRYPT_KMS_PROVIDER_GCP); ++ ASSERT_STREQUAL(kek.kmsid, "gcp"); ++ ASSERT_STREQUAL(kek.provider.gcp.key_ring, "ring"); ++ ASSERT_STREQUAL(kek.provider.gcp.key_name, "name"); ++ ++ /* Cleanup frees the GCP fields, matching the tag. */ ++ _mongocrypt_kek_cleanup(&kek); ++ mongocrypt_status_destroy(status); ++} ++ + void _mongocrypt_tester_install_kek(_mongocrypt_tester_t *tester) { + INSTALL_TEST(test_mongocrypt_kek_parsing); ++ INSTALL_TEST(test_mongocrypt_kek_parse_failure_leaves_kek_intact); + } +-- +2.39.5 + diff -Nru libmongocrypt-1.13.2/debian/patches/0011_CVE-2026-114655.patch libmongocrypt-1.13.2/debian/patches/0011_CVE-2026-114655.patch --- libmongocrypt-1.13.2/debian/patches/0011_CVE-2026-114655.patch 1970-01-01 00:00:00.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/0011_CVE-2026-114655.patch 2026-11-03 20:28:53.000000000 +0000 @@ -0,0 +1,1169 @@ +From a83b565593e906710a7d61121ac44ffc190eb21c Mon Sep 17 00:00:00 2001 +From: Kevin Albertson +Date: Wed, 21 Oct 2026 13:48:27 -0400 +Subject: [PATCH] MONGOCRYPT-963 Inputs not sanitized for KMS HTTP requests + +--- + kms-message/src/kms_azure_request.c | 66 ++++ + kms-message/src/kms_encrypt_request.c | 33 ++ + kms-message/src/kms_gcp_request.c | 66 +++- + kms-message/src/kms_request.c | 128 ++++++++ + kms-message/test/test_kms_request.c | 512 ++++++++++++++++++++++++++++++++++ + test/test-gcp-auth.c | 97 ++++++ + test/test-mongocrypt-datakey.c | 26 + + test/test-mongocrypt.c | 2 + 8 files changed, 917 insertions(+), 13 deletions(-) + +--- a/kms-message/src/kms_azure_request.c ++++ b/kms-message/src/kms_azure_request.c +@@ -21,6 +21,54 @@ + #include "kms_request_opt_private.h" + #include "kms_request_str.h" + ++#include ++ ++/* _check_path_segment returns true if `value` is safe to use as a URL path ++ * segment. ++ * ++ * Key identifiers may originate from key vault documents or caller-supplied ++ * options. They are formatted into the request path, so an unescaped '/', '?', ++ * '#', space, or CR/LF would rewrite the request target or inject header lines ++ * into the request. A '/' or ".." can address a different key, still ++ * authenticated with the caller's access token. ++ * ++ * Only the RFC 3986 unreserved characters are permitted. This is a superset of ++ * what Azure itself accepts, so it does not reject an identifier that would ++ * otherwise work: ++ * - A key name must match `[a-zA-Z0-9-]{1,127}`. See ++ * https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#objects-identifiers-and-versioning ++ * - A key version is a 32 character hex string. ++ * - A tenant ID is a GUID, or a domain name (e.g. "contoso.onmicrosoft.com"). ++ * ++ * Each identifier must also contain an alphanumeric character. No valid ++ * identifier lacks one, and this keeps a value of only dots from forming a ++ * dot-segment ("..") in the request path. ++ * ++ * `value` is not included in the error message: it is untrusted, and the error ++ * may be logged. */ ++static bool ++_check_path_segment (kms_request_t *req, const char *name, const char *value) ++{ ++ const char *c; ++ bool has_alnum = false; ++ ++ for (c = value; *c != '\0'; c++) { ++ if (!((*c >= '0' && *c <= '9') || (*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z')) && *c != '-' && *c != '.' && ++ *c != '_' && *c != '~') { ++ KMS_ERROR (req, "Invalid character in Azure KMS identifier: %s", name); ++ return false; ++ } ++ if ((*c >= '0' && *c <= '9') || (*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z')) { ++ has_alnum = true; ++ } ++ } ++ if (!has_alnum) { ++ KMS_ERROR (req, "Invalid Azure KMS identifier: %s", name); ++ return false; ++ } ++ return true; ++} ++ + /* + * Request has the following form: + * +@@ -65,6 +113,14 @@ + goto done; + } + ++ /* Check the path segments before checking req for an error. tenant_id is ++ * formatted into the request path above, so an invalid value may have ++ * already failed the generic request line validation in kms_request_new. ++ * Report the more specific error naming the field. */ ++ if (!_check_path_segment (req, "tenantId", tenant_id)) { ++ goto done; ++ } ++ + if (kms_request_get_error (req)) { + goto done; + } +@@ -123,6 +179,16 @@ + goto done; + } + ++ /* Check the path segments before checking req for an error. See the ++ * comment in kms_azure_request_oauth_new. */ ++ if (!_check_path_segment (req, "keyName", key_name) || ++ /* An empty key version is treated the same as an absent one: both ++ * produce the same request path. */ ++ (key_version && *key_version && ++ !_check_path_segment (req, "keyVersion", key_version))) { ++ goto done; ++ } ++ + if (kms_request_get_error (req)) { + goto done; + } +--- a/kms-message/src/kms_encrypt_request.c ++++ b/kms-message/src/kms_encrypt_request.c +@@ -20,6 +20,35 @@ + #include "kms_message/kms_b64.h" + #include "kms_request_str.h" + ++#define KMS_PRINTABLE_ASCII_START ' ' ++#define KMS_PRINTABLE_ASCII_END '~' ++ ++/* _key_id_chars_are_valid returns true if `key_id` is safe to interpolate into the JSON body of the AWS ++ * Encrypt request. Reject non-printable ASCII, double quotes, and backslashes: those end the JSON string ++ * and let the caller add request members of their own, which are then signed with the SigV4 signature. ++ * ++ * A key identifier may be a key ID, an ARN, or an alias, none of which contain those characters. ++ * ++ * `key_id` is not included in the error message: it is untrusted, and the error may be logged. ++ * ++ * This duplicates _oauth_field_chars_are_valid in kms_gcp_request.c. */ ++static bool ++_key_id_chars_are_valid (kms_request_t *req, const char *key_id) ++{ ++ const char *i; ++ ++ for (i = key_id; *i != '\0'; i++) { ++ const unsigned char uc = (unsigned char) *i; ++ ++ if (uc < KMS_PRINTABLE_ASCII_START || uc > KMS_PRINTABLE_ASCII_END || ++ uc == '"' || uc == '\\') { ++ KMS_ERROR (req, "Invalid character in AWS KMS key identifier"); ++ return false; ++ } ++ } ++ return true; ++} ++ + kms_request_t * + kms_encrypt_request_new (const uint8_t *plaintext, + size_t plaintext_length, +@@ -36,6 +65,10 @@ + goto done; + } + ++ if (!_key_id_chars_are_valid (request, key_id)) { ++ goto done; ++ } ++ + if (!(kms_request_add_header_field ( + request, "Content-Type", "application/x-amz-json-1.1") && + kms_request_add_header_field ( +--- a/kms-message/src/kms_gcp_request.c ++++ b/kms-message/src/kms_gcp_request.c +@@ -20,13 +20,34 @@ + #include "kms_message_private.h" + #include "kms_request_opt_private.h" + +-#include +- + /* Set a default expiration of 5 minutes for JSON Web Tokens (GCP allows up to + * one hour) */ + #define JWT_EXPIRATION_SECS 5 * 60 + #define SIGNATURE_LEN 256 + ++#define KMS_PRINTABLE_ASCII_START ' ' ++#define KMS_PRINTABLE_ASCII_END '~' ++ ++/* _oauth_field_chars_are_valid returns true if `value` is safe to interpolate into the JWT claim set of the ++ * GCP OAuth request. Reject non-printable ASCII, double quotes, and backslashes. ++ * ++ * `value` is not included in the error message: it is untrusted, and the error may be logged. */ ++static bool ++_oauth_field_chars_are_valid(kms_request_t *req, const char *name, const char *value) ++{ ++ const char *i; ++ ++ for (i = value; *i != '\0'; i++) { ++ const unsigned char uc = (unsigned char)*i; ++ ++ if (uc < KMS_PRINTABLE_ASCII_START || uc > KMS_PRINTABLE_ASCII_END || uc == '"' || uc == '\\') { ++ KMS_ERROR(req, "Invalid character in GCP OAuth request: %s", name); ++ return false; ++ } ++ } ++ return true; ++} ++ + kms_request_t * + kms_gcp_request_oauth_new (const char *host, + const char *email, +@@ -58,6 +79,11 @@ + goto done; + } + ++ if (!(_oauth_field_chars_are_valid(req, "email", email) && _oauth_field_chars_are_valid(req, "audience", audience) && ++ _oauth_field_chars_are_valid(req, "scope", scope))) { ++ goto done; ++ } ++ + /* Produce the signed JWT .. */ + issued_at = time (NULL); +@@ -163,18 +189,31 @@ + * - location is a Cloud location name (e.g. "global", "us-east1"). + * - keyVersion is a positive integer. + * ++ * Each identifier must also contain an alphanumeric character. No valid identifier lacks one, and ++ * this keeps a value of only dots from forming a dot-segment ("..") in the request path. ++ * + * `value` is not included in the error message: it is untrusted, and the error may be logged. */ + static bool + _check_key_identifier (kms_request_t *req, const char *name, const char *value) + { + const char *c; ++ bool has_alnum = false; + + for (c = value; *c != '\0'; c++) { +- if (!isalnum ((unsigned char) *c) && *c != '-' && *c != '.' && *c != '_' && +- *c != '~') { ++ if (!((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || ++ (*c >= '0' && *c <= '9') || *c == '-' || *c == '.' || *c == '_' || ++ *c == '~')) { + KMS_ERROR (req, "Invalid character in GCP KMS key identifier: %s", name); + return false; + } ++ if ((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || ++ (*c >= '0' && *c <= '9')) { ++ has_alnum = true; ++ } ++ } ++ if (!has_alnum) { ++ KMS_ERROR (req, "Invalid GCP KMS key identifier: %s", name); ++ return false; + } + return true; + } +@@ -222,15 +261,22 @@ + goto done; + } + +- if (kms_request_get_error (req)) { +- goto done; +- } +- ++ /* Check the key identifiers before checking req for an error. The ++ * identifiers are formatted into the request path above, so an invalid ++ * identifier may have already failed the generic request line validation in ++ * kms_request_new. Report the more specific error naming the field. */ + if (!_check_key_identifier (req, "projectId", project_id) || + !_check_key_identifier (req, "location", location) || + !_check_key_identifier (req, "keyRing", key_ring_name) || + !_check_key_identifier (req, "keyName", key_name) || +- (key_version && !_check_key_identifier (req, "keyVersion", key_version))) { ++ /* An empty key version is treated the same as an absent one: it is ++ * omitted from the path above, so there is nothing to validate. */ ++ (key_version && *key_version && ++ !_check_key_identifier (req, "keyVersion", key_version))) { ++ goto done; ++ } ++ ++ if (kms_request_get_error (req)) { + goto done; + } + +@@ -325,4 +371,4 @@ + ciphertext, + ciphertext_len, + opt); +-} +\ No newline at end of file ++} +--- a/kms-message/src/kms_request.c ++++ b/kms-message/src/kms_request.c +@@ -64,6 +64,83 @@ + return true; + } + ++/* Callers interpolate externally supplied strings (key names, hosts, access ++ * tokens) into requests. Validate that they cannot alter the structure of the ++ * resulting HTTP message: a CR or LF forges an additional header, terminates ++ * the header section, or pipelines an entire second request, and a space in the ++ * request line changes the request target. */ ++ ++/* Checks the method of the "METHOD path?query HTTP/1.1" request line. */ ++static bool ++check_method (kms_request_t *request, const char *method) ++{ ++ if (!*method) { ++ KMS_ERROR (request, "HTTP method must not be empty"); ++ return false; ++ } ++ if (strpbrk (method, " \r\n")) { ++ KMS_ERROR (request, "HTTP method contains invalid characters"); ++ return false; ++ } ++ return true; ++} ++ ++/* Checks the request target (path and query) of the request line. ++ * ++ * A space is prohibited because it ends the request target, except for AWS: the ++ * SigV4 conformance suite requires a literal space in the path to be sent ++ * verbatim and signed as %20 (normalize-path/get-space). AWS request targets in ++ * libmongocrypt are fixed strings, not interpolated from external input. */ ++static bool ++check_request_target (kms_request_t *request, ++ const char *what, ++ const char *str) ++{ ++ const char *prohibited = ++ request->provider == KMS_REQUEST_PROVIDER_AWS ? "\r\n" : " \r\n"; ++ ++ if (strpbrk (str, prohibited)) { ++ KMS_ERROR (request, "HTTP %s contains invalid characters", what); ++ return false; ++ } ++ return true; ++} ++ ++/* Checks a header name, or a header value that is not permitted to fold. */ ++static bool ++check_header_value (kms_request_t *request, const char *what, const char *str) ++{ ++ if (strpbrk (str, "\r\n")) { ++ KMS_ERROR (request, "HTTP %s contains invalid characters", what); ++ return false; ++ } ++ return true; ++} ++ ++/* Checks an already stored header value. ++ * ++ * A CRLF is permitted only as a line fold: the continuation line must begin ++ * with a space or horizontal tab. Line folding is deprecated by RFC 7230 ++ * section 3.2.4, but is still accepted here because the AWS SigV4 test suite ++ * exercises it (get-header-value-multiline) by way of ++ * kms_request_append_header_field_value, which is the only API able to produce ++ * a folded value. */ ++static bool ++check_stored_header_value (kms_request_t *request, ++ const kms_request_str_t *value) ++{ ++ const char *p = value->str; ++ ++ while ((p = strpbrk (p, "\r\n"))) { ++ if (p[0] != '\r' || p[1] != '\n' || (p[2] != ' ' && p[2] != '\t')) { ++ KMS_ERROR (request, "HTTP header value contains invalid characters"); ++ return false; ++ } ++ p += 2; ++ } ++ return true; ++} ++ + kms_request_t * + kms_request_new (const char *method, + const char *path_and_query, +@@ -113,6 +190,18 @@ + request->header_fields = kms_kv_list_new (); + request->auto_content_length = true; + ++ /* Validate once all members are initialized, so the request remains safe to ++ * destroy. */ ++ if (request->failed) { ++ return request; ++ } ++ if (!check_method (request, method)) { ++ return request; ++ } ++ if (!check_request_target (request, "path", path_and_query)) { ++ return request; ++ } ++ + /* For AWS KMS requests, add a X-Amz-Date header. */ + if (request->provider == KMS_REQUEST_PROVIDER_AWS && + !kms_request_set_date (request, NULL)) { +@@ -214,6 +303,11 @@ + if (!check_and_prohibit_kmip (request)) { + return false; + } ++ /* region and service reach the wire in the SigV4 Authorization header's ++ * credential scope, and in the Host header derived by finalize(). */ ++ if (!check_header_value (request, "region", region)) { ++ return false; ++ } + kms_request_str_set_chars (request->region, region, -1); + return true; + } +@@ -224,6 +318,9 @@ + if (!check_and_prohibit_kmip (request)) { + return false; + } ++ if (!check_header_value (request, "service", service)) { ++ return false; ++ } + kms_request_str_set_chars (request->service, service, -1); + return true; + } +@@ -234,6 +331,10 @@ + if (!check_and_prohibit_kmip (request)) { + return false; + } ++ /* access_key_id reaches the wire in the SigV4 Authorization header. */ ++ if (!check_header_value (request, "access key id", akid)) { ++ return false; ++ } + kms_request_str_set_chars (request->access_key_id, akid, -1); + return true; + } +@@ -261,6 +362,15 @@ + return false; + } + ++ /* This API cannot produce a folded value, so CR and LF are prohibited ++ * outright. Use kms_request_append_header_field_value to fold. */ ++ if (!check_header_value (request, "header name", field_name)) { ++ return false; ++ } ++ if (!check_header_value (request, "header value", value)) { ++ return false; ++ } ++ + k = kms_request_str_new_from_chars (field_name, -1); + v = kms_request_str_new_from_chars (value, -1); + kms_kv_list_add (request->header_fields, k, v); +@@ -425,6 +535,7 @@ + static bool + finalize (kms_request_t *request) + { ++ size_t i; + kms_kv_list_t *lst; + kms_request_str_t *k; + kms_request_str_t *v; +@@ -468,6 +579,23 @@ + kms_request_str_destroy (v); + } + ++ /* Every serializing entry point funnels through here, so validate the ++ * assembled request as a backstop: the setters above and ++ * kms_request_append_header_field_value can all reach the wire. */ ++ if (!check_method (request, request->method->str) || ++ !check_request_target (request, "path", request->path->str) || ++ !check_request_target (request, "query", request->query->str)) { ++ return false; ++ } ++ for (i = 0; i < lst->len; i++) { ++ if (!check_header_value (request, "header name", lst->kvs[i].key->str)) { ++ return false; ++ } ++ if (!check_stored_header_value (request, lst->kvs[i].value)) { ++ return false; ++ } ++ } ++ + return true; + } + +--- a/kms-message/test/test_kms_request.c ++++ b/kms-message/test/test_kms_request.c +@@ -35,6 +35,7 @@ + #include + #include + #include "kms_message/kms_azure_request.h" ++#include "kms_message/kms_gcp_request.h" + #include "kms_message/kms_b64.h" + #include "hexlify.h" + #include "kms_request_str.h" +@@ -1196,6 +1197,485 @@ + } + } + ++/* MONGOCRYPT-963: CR/LF in strings interpolated into an HTTP request must not ++ * reach the wire, where they would forge headers, terminate the header ++ * section, or pipeline an entire second request. */ ++ ++#define INJECTED "a\r\nX-Injected: injected" ++ ++#define COUNTOF(_arr) (sizeof (_arr) / sizeof ((_arr)[0])) ++ ++/* Asserts the request was rejected. If a future change escapes rather than ++ * rejects, at minimum no header may have been forged. */ ++static void ++assert_injection_rejected (kms_request_t *req, const char *label) ++{ ++ char *req_str = kms_request_to_string (req); ++ ++ if (req_str) { ++ TEST_PRINTF ("injected into %s, got request:\n%s\n", label, req_str); ++ ASSERT (NULL == strstr (req_str, "X-Injected")); ++ free (req_str); ++ TEST_PRINTF ("injected into %s: expected an error, got none\n", label); ++ ASSERT (false); ++ } ++ ASSERT (kms_request_get_error (req)); ++} ++ ++static kms_request_opt_t * ++opt_new_for_provider (kms_request_provider_t provider) ++{ ++ kms_request_opt_t *opt = kms_request_opt_new (); ++ ASSERT (opt); ++ ASSERT (kms_request_opt_set_provider (opt, provider)); ++ return opt; ++} ++ ++static void ++test_azure_request_crlf_injection (void) ++{ ++ uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; ++ size_t i; ++ ++ struct { ++ const char *label; ++ const char *host; ++ const char *access_token; ++ const char *key_name; ++ const char *key_version; ++ } wrapkey_cases[] = { ++ {"host", INJECTED, "token", "key-name", "key-version"}, ++ {"access_token", "host", INJECTED, "key-name", "key-version"}, ++ }; ++ ++ struct { ++ const char *label; ++ const char *host; ++ const char *tenant_id; ++ } oauth_cases[] = { ++ {"host", INJECTED, "tenant-id"}, ++ }; ++ ++ for (i = 0; i < COUNTOF (wrapkey_cases); i++) { ++ kms_request_opt_t *opt = ++ opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = ++ kms_azure_request_wrapkey_new (wrapkey_cases[i].host, ++ wrapkey_cases[i].access_token, ++ wrapkey_cases[i].key_name, ++ wrapkey_cases[i].key_version, ++ data, ++ sizeof (data), ++ opt); ++ assert_injection_rejected (req, wrapkey_cases[i].label); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ for (i = 0; i < COUNTOF (oauth_cases); i++) { ++ kms_request_opt_t *opt = ++ opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = ++ kms_azure_request_oauth_new (oauth_cases[i].host, ++ "scope", ++ oauth_cases[i].tenant_id, ++ "client-id", ++ "client-secret", ++ opt); ++ assert_injection_rejected (req, oauth_cases[i].label); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++} ++ ++/* MONGOCRYPT-963: Azure key identifiers are formatted into the request path. ++ * A metacharacter can rewrite the request target without any CR/LF. */ ++static void ++test_azure_request_path_injection (void) ++{ ++ uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; ++ size_t i, j; ++ ++ const char *injections[] = { ++ "kn?api-version=7.0&x=", /* hijacks the query, downgrading api-version */ ++ "../../../keys/attacker-key", /* addresses a different key */ ++ "kn/kv/wrapkey?api-version=7.1&x=", /* adds path segments */ ++ "kn kv", /* ends the request target */ ++ "kn#fragment", ++ "kn%2Fkv", /* already-encoded input is not accepted either */ ++ }; ++ ++ /* keyName and keyVersion are both path segments. */ ++ for (i = 0; i < COUNTOF (injections); i++) { ++ for (j = 0; j < 2; j++) { ++ kms_request_opt_t *opt = ++ opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = kms_azure_request_wrapkey_new ( ++ "host", ++ "token", ++ j == 0 ? injections[i] : "key-name", ++ j == 0 ? "key-version" : injections[i], ++ data, ++ sizeof (data), ++ opt); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR ( ++ req, ++ j == 0 ? "Invalid character in Azure KMS identifier: keyName" ++ : "Invalid character in Azure KMS identifier: keyVersion"); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ /* tenantId is a path segment of the OAuth request. */ ++ kms_request_opt_t *opt = ++ opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = kms_azure_request_oauth_new ( ++ "host", "scope", injections[i], "client-id", "client-secret", opt); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR (req, ++ "Invalid character in Azure KMS identifier: tenantId"); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ /* A GUID tenant ID, a hex key version, and an absent key version are ++ * accepted. */ ++ { ++ kms_request_opt_t *opt = ++ opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = kms_azure_request_wrapkey_new ( ++ "host", ++ "token", ++ "my-key-name", ++ "9f8e7d6c5b4a39281706f5e4d3c2b1a0", ++ data, ++ sizeof (data), ++ opt); ++ ASSERT_REQUEST_OK (req); ++ char *req_str = kms_request_to_string (req); ++ ASSERT (req_str); ++ ASSERT (strstr (req_str, ++ "POST /keys/my-key-name/9f8e7d6c5b4a39281706f5e4d3c2b1a0" ++ "/wrapkey?api-version=7.1 HTTP/1.1")); ++ free (req_str); ++ kms_request_destroy (req); ++ ++ req = kms_azure_request_wrapkey_new ( ++ "host", "token", "my-key-name", NULL, data, sizeof (data), opt); ++ ASSERT_REQUEST_OK (req); ++ req_str = kms_request_to_string (req); ++ ASSERT (req_str); ++ ASSERT (strstr (req_str, "POST /keys/my-key-name//wrapkey")); ++ free (req_str); ++ kms_request_destroy (req); ++ ++ req = kms_azure_request_oauth_new ("host", ++ "scope", ++ "contoso.onmicrosoft.com", ++ "client-id", ++ "client-secret", ++ opt); ++ ASSERT_REQUEST_OK (req); ++ req_str = kms_request_to_string (req); ++ ASSERT (req_str); ++ ASSERT (strstr (req_str, ++ "POST /contoso.onmicrosoft.com/oauth2/v2.0/token")); ++ free (req_str); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++} ++ ++/* An Azure key identifier must contain an alphanumeric character, so a value of only ++ * dots cannot form a dot-segment in the request path. An empty key version is ++ * treated as absent. */ ++static void ++test_azure_request_dot_segment (void) ++{ ++ uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; ++ const char *values[] = {"..", ".", ""}; ++ size_t i; ++ ++ /* keyName is a path segment of the wrapkey request. */ ++ for (i = 0; i < COUNTOF (values); i++) { ++ kms_request_opt_t *opt = opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = kms_azure_request_wrapkey_new ("host", ++ "token", ++ values[i], ++ "9f8e7d6c5b4a39281706f5e4d3c2b1a0", ++ data, ++ sizeof (data), ++ opt); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR (req, "Invalid Azure KMS identifier: keyName"); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ /* keyVersion is a path segment of the wrapkey request. An empty value is ++ * treated like an absent one, which adds no segment to validate. */ ++ { ++ const char *dot_values[] = {"..", "."}; ++ ++ for (i = 0; i < COUNTOF (dot_values); i++) { ++ kms_request_opt_t *opt = ++ opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = kms_azure_request_wrapkey_new ( ++ "host", "token", "key-name", dot_values[i], data, sizeof (data), opt); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR (req, "Invalid Azure KMS identifier: keyVersion"); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ kms_request_opt_t *opt = ++ opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = kms_azure_request_wrapkey_new ( ++ "host", "token", "key-name", "", data, sizeof (data), opt); ++ ASSERT_REQUEST_OK (req); ++ char *req_str = kms_request_to_string (req); ++ ASSERT (req_str); ++ ASSERT (strstr (req_str, "POST /keys/key-name//wrapkey")); ++ free (req_str); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ /* tenantId is a path segment of the OAuth request. */ ++ for (i = 0; i < COUNTOF (values); i++) { ++ kms_request_opt_t *opt = opt_new_for_provider (KMS_REQUEST_PROVIDER_AZURE); ++ kms_request_t *req = ++ kms_azure_request_oauth_new ("host", "scope", values[i], "client-id", "client-secret", opt); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR (req, "Invalid Azure KMS identifier: tenantId"); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++} ++ ++/* The GCP key identifier fields are validated by _check_key_identifier and ++ * covered by MONGOCRYPT-960. Only the header fields are checked here. */ ++static void ++test_gcp_request_crlf_injection (void) ++{ ++ uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; ++ size_t i; ++ ++ struct { ++ const char *label; ++ const char *host; ++ const char *access_token; ++ const char *project_id; ++ const char *location; ++ const char *key_ring_name; ++ const char *key_name; ++ const char *key_version; ++ } cases[] = { ++ {"host", INJECTED, "token", "project", "location", "ring", "key", "1"}, ++ {"access_token", "host", INJECTED, "project", "location", "ring", "key", "1"}, ++ }; ++ ++ for (i = 0; i < COUNTOF (cases); i++) { ++ kms_request_opt_t *opt = opt_new_for_provider (KMS_REQUEST_PROVIDER_GCP); ++ kms_request_t *req = kms_gcp_request_encrypt_new (cases[i].host, ++ cases[i].access_token, ++ cases[i].project_id, ++ cases[i].location, ++ cases[i].key_ring_name, ++ cases[i].key_name, ++ cases[i].key_version, ++ data, ++ sizeof (data), ++ opt); ++ assert_injection_rejected (req, cases[i].label); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++} ++ ++/* A GCP key identifier must contain an alphanumeric character, so a value of only ++ * dots cannot form a dot-segment in the request path. An empty key version is ++ * treated as absent. */ ++static void ++test_gcp_request_dot_segment (void) ++{ ++ uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; ++ const char *values[] = {"..", ".", ""}; ++ size_t i; ++ ++ for (i = 0; i < COUNTOF (values); i++) { ++ kms_request_opt_t *opt = opt_new_for_provider (KMS_REQUEST_PROVIDER_GCP); ++ kms_request_t *req = kms_gcp_request_encrypt_new ("host", ++ "token", ++ "project", ++ "location", ++ values[i], ++ "key", ++ "1", ++ data, ++ sizeof (data), ++ opt); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR (req, "Invalid GCP KMS key identifier: keyRing"); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ { ++ kms_request_opt_t *opt = opt_new_for_provider (KMS_REQUEST_PROVIDER_GCP); ++ kms_request_t *req = kms_gcp_request_encrypt_new ("host", ++ "token", ++ "project", ++ "location", ++ "ring", ++ "key", ++ "", ++ data, ++ sizeof (data), ++ opt); ++ ASSERT_REQUEST_OK (req); ++ char *req_str = kms_request_to_string (req); ++ ASSERT (req_str); ++ ASSERT (strstr (req_str, "/keyRings/ring/cryptoKeys/key:encrypt")); ++ free (req_str); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++} ++ ++/* libmongocrypt adds the AWS session token and Host header itself, and the ++ * access key id, region, and service reach the wire in the SigV4 Authorization ++ * and derived Host headers. */ ++static void ++test_aws_request_crlf_injection (void) ++{ ++ kms_request_t *req; ++ ++ req = kms_request_new ("POST", "/", NULL); ++ ASSERT (!kms_request_add_header_field (req, "Host", INJECTED)); ++ ASSERT_REQUEST_ERROR (req, "HTTP header value contains invalid characters"); ++ kms_request_destroy (req); ++ ++ req = kms_request_new ("POST", "/", NULL); ++ ASSERT (!kms_request_add_header_field (req, INJECTED, "value")); ++ ASSERT_REQUEST_ERROR (req, "HTTP header name contains invalid characters"); ++ kms_request_destroy (req); ++ ++ req = kms_request_new ("POST", "/", NULL); ++ ASSERT (!kms_request_set_access_key_id (req, INJECTED)); ++ ASSERT_REQUEST_ERROR (req, "HTTP access key id contains invalid characters"); ++ kms_request_destroy (req); ++ ++ req = kms_request_new ("POST", "/", NULL); ++ ASSERT (!kms_request_set_region (req, INJECTED)); ++ ASSERT_REQUEST_ERROR (req, "HTTP region contains invalid characters"); ++ kms_request_destroy (req); ++ ++ req = kms_request_new ("POST", "/", NULL); ++ ASSERT (!kms_request_set_service (req, INJECTED)); ++ ASSERT_REQUEST_ERROR (req, "HTTP service contains invalid characters"); ++ kms_request_destroy (req); ++ ++ /* The request line. */ ++ req = kms_request_new (INJECTED, "/", NULL); ++ ASSERT_REQUEST_ERROR (req, "HTTP method contains invalid characters"); ++ kms_request_destroy (req); ++ ++ req = kms_request_new ("", "/", NULL); ++ ASSERT_REQUEST_ERROR (req, "HTTP method must not be empty"); ++ kms_request_destroy (req); ++ ++ req = kms_request_new ("POST", "/" INJECTED, NULL); ++ ASSERT_REQUEST_ERROR (req, "HTTP path contains invalid characters"); ++ kms_request_destroy (req); ++} ++ ++/* A CRLF in a header value is permitted only as a line fold: the continuation ++ * line must begin with a space or horizontal tab. Folding is deprecated by RFC ++ * 7230 section 3.2.4, but the AWS SigV4 suite exercises it ++ * (get-header-value-multiline). */ ++/* MONGOCRYPT-968: the AWS key identifier is interpolated into the JSON body of the ++ * Encrypt request. A double quote ends the JSON string and adds request members, ++ * which are then covered by the SigV4 signature. */ ++static void ++test_aws_encrypt_key_id_injection (void) ++{ ++ size_t i; ++ uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; ++ const char *injections[] = { ++ "x\", \"EncryptionContext\": {\"a\":\"b\"}, \"KeyId\": \"attacker-key", ++ "x\\", ++ "x\ny", ++ }; ++ ++ for (i = 0; i < COUNTOF (injections); i++) { ++ kms_request_opt_t *opt = kms_request_opt_new (); ++ ASSERT (opt); ++ kms_request_t *req = ++ kms_encrypt_request_new (data, sizeof (data), injections[i], opt); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR (req, "Invalid character in AWS KMS key identifier"); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++ ++ /* A key ID, an ARN, and an alias are accepted. */ ++ const char *valid[] = { ++ "1234abcd-12ab-34cd-56ef-1234567890ab", ++ "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab", ++ "alias/ExampleAlias", ++ }; ++ ++ for (i = 0; i < COUNTOF (valid); i++) { ++ kms_request_opt_t *opt = kms_request_opt_new (); ++ ASSERT (opt); ++ kms_request_t *req = ++ kms_encrypt_request_new (data, sizeof (data), valid[i], opt); ++ ASSERT_REQUEST_OK (req); ++ ASSERT (kms_request_set_region (req, "us-east-1")); ++ ASSERT (kms_request_set_service (req, "kms")); ++ char *req_str = kms_request_to_string (req); ++ ASSERT (req_str); ++ ASSERT (strstr (req_str, valid[i])); ++ free (req_str); ++ kms_request_destroy (req); ++ kms_request_opt_destroy (opt); ++ } ++} ++ ++static void ++test_header_field_value_folding (void) ++{ ++ kms_request_t *req; ++ char *req_str; ++ ++ /* A valid fold is preserved. */ ++ req = kms_request_new ("POST", "/", NULL); ++ ASSERT (kms_request_add_header_field (req, "My-Header", "value1")); ++ ASSERT (kms_request_append_header_field_value (req, "\r\n", 2)); ++ ASSERT (kms_request_append_header_field_value (req, " value2", 8)); ++ ASSERT (kms_request_set_region (req, "region")); ++ ASSERT (kms_request_set_service (req, "service")); ++ req_str = kms_request_to_string (req); ++ ASSERT_REQUEST_OK (req); ++ ASSERT (req_str); ++ ASSERT (strstr (req_str, "My-Header:value1\r\n value2\r\n")); ++ free (req_str); ++ kms_request_destroy (req); ++ ++ /* A fold whose continuation line forges a header is rejected. */ ++ req = kms_request_new ("POST", "/", NULL); ++ ASSERT (kms_request_add_header_field (req, "My-Header", "value1")); ++ ASSERT (kms_request_append_header_field_value ( ++ req, INJECTED, strlen (INJECTED))); ++ ASSERT (kms_request_set_region (req, "region")); ++ ASSERT (kms_request_set_service (req, "service")); ++ ASSERT (NULL == kms_request_to_string (req)); ++ ASSERT_REQUEST_ERROR (req, "HTTP header value contains invalid characters"); ++ kms_request_destroy (req); ++} ++ + #define RUN_TEST(_func) \ + do { \ + if (!selector || 0 == kms_strcasecmp (#_func, selector)) { \ +@@ -1224,6 +1704,29 @@ + extern void kms_kmip_response_parser_excess_test (void); + extern void kms_kmip_response_parser_notenough_test (void); + ++/* Regression test for MONGOCRYPT-965. */ ++static void ++kms_gcp_oauth_invalid_scope_test (void) ++{ ++ kms_request_opt_t* const opt = kms_request_opt_new (); ++ kms_request_opt_set_provider (opt, KMS_REQUEST_PROVIDER_GCP); ++ ++ /* The private key is unused: the request fails before it is signed. */ ++ kms_request_t* const request = kms_gcp_request_oauth_new ( ++ "oauth2.googleapis.com", ++ "test@example.com", ++ "https://oauth2.googleapis.com/token", ++ "https://www.example.com\", \"sub\": \"victim@example.com/auth/cloudkms", ++ NULL /* private_key_data */, ++ 0 /* private_key_len */, ++ opt); ++ ASSERT_CONTAINS (kms_request_get_error (request), ++ "Invalid character in GCP OAuth request: scope"); ++ ++ kms_request_destroy (request); ++ kms_request_opt_destroy (opt); ++} ++ + int + main (int argc, char *argv[]) + { +@@ -1289,7 +1792,16 @@ + RUN_TEST (kms_kmip_response_parser_excess_test); + RUN_TEST (kms_kmip_response_parser_notenough_test); + RUN_TEST (test_request_newlines); ++ RUN_TEST (test_azure_request_crlf_injection); ++ RUN_TEST (test_azure_request_path_injection); ++ RUN_TEST (test_azure_request_dot_segment); ++ RUN_TEST (test_gcp_request_crlf_injection); ++ RUN_TEST (test_gcp_request_dot_segment); ++ RUN_TEST (test_aws_request_crlf_injection); ++ RUN_TEST (test_aws_encrypt_key_id_injection); ++ RUN_TEST (test_header_field_value_folding); + RUN_TEST (test_kms_util); ++ RUN_TEST (kms_gcp_oauth_invalid_scope_test); + + + if (!ran_tests) { +--- a/test/test-gcp-auth.c ++++ b/test/test-gcp-auth.c +@@ -292,8 +292,105 @@ + mongocrypt_destroy(crypt); + } + ++// Test a keyRing of ".." in a key vault document. A dot-segment in the request path may be ++// normalized by a server or proxy to address a different key. Regression test for MONGOCRYPT-963. ++static void _test_dot_segment_in_gcp_keyring(_mongocrypt_tester_t *tester) { ++ mongocrypt_t *crypt; ++ mongocrypt_ctx_t *ctx; ++ mongocrypt_binary_t *uuid; ++ const char *uuid_data = "\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"; ++ ++ // clang-format off ++ const char *key_doc = BSON_STR({ ++ "status" : 1, ++ "_id" : {"$binary" : {"base64" : "YWFhYWFhYWFhYWFhYWFhYQ==", "subType" : "04"}}, ++ "masterKey" : { ++ "provider" : "gcp", ++ "projectId" : "test", ++ "location" : "global", ++ "keyRing" : "..", ++ "keyName" : "test" ++ }, ++ "updateDate" : {"$date" : {"$numberLong" : "1557827033449"}}, ++ "creationDate" : {"$date" : {"$numberLong" : "1557827033449"}}, ++ "keyMaterial" : { ++ "$binary" : { ++ "base64" : "PwwOESbKs57YTJtGSCsuAJbv9VWRHjLdPdziUVxH0K9woZUka4SghSwJlw9n6TU/dYHFODvSa4p7bfKzS6U8kGFvOnd7LcPU63IkTek4qZEbGpTRwI5lMT4FFWdpYpUf", ++ "subType" : "00" ++ } ++ } ++ }); ++ // clang-format on ++ ++ crypt = mongocrypt_new(); ++ ASSERT_OK(mongocrypt_setopt_kms_providers(crypt, TEST_BSON("{'gcp': {'accessToken': 'foobar'}}")), crypt); ++ ASSERT_OK(_mongocrypt_init_for_test(crypt), crypt); ++ ctx = mongocrypt_ctx_new(crypt); ++ uuid = mongocrypt_binary_new_from_data((uint8_t *)uuid_data, UUID_LEN); ++ ASSERT_OK(mongocrypt_ctx_setopt_key_id(ctx, uuid), ctx); ++ ASSERT_OK(mongocrypt_ctx_setopt_algorithm(ctx, MONGOCRYPT_ALGORITHM_DETERMINISTIC_STR, -1), ctx); ++ ASSERT_OK(mongocrypt_ctx_explicit_encrypt_init(ctx, TEST_BSON("{'v': 1}")), ctx); ++ ++ ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_MONGO_KEYS); ++ /* Constructing the KMS request fails. No request is sent. */ ++ ASSERT_FAILS(mongocrypt_ctx_mongo_feed(ctx, TEST_BSON(key_doc)), ++ ctx, ++ "Invalid GCP KMS key identifier: keyRing"); ++ ++ mongocrypt_binary_destroy(uuid); ++ mongocrypt_ctx_destroy(ctx); ++ mongocrypt_destroy(crypt); ++} ++ ++// Test a double quote in a key vault document's masterKey.endpoint. Regression test for MONGOCRYPT-965. ++static void _test_quote_in_gcp_endpoint(_mongocrypt_tester_t *tester) { ++ const char *const uuid_data = "\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61"; ++ ++ // A key vault document whose masterKey.endpoint contains a double quote. ++ // ++ // clang-format off ++ const char * const key_doc = BSON_STR({ ++ "status" : 1, ++ "_id" : {"$binary" : {"base64" : "YWFhYWFhYWFhYWFhYWFhYQ==", "subType" : "04"}}, ++ "masterKey" : { ++ "provider" : "gcp", ++ "projectId" : "test", ++ "location" : "global", ++ "keyRing" : "test", ++ "keyName" : "test", ++ "endpoint" : "example.com\", \"sub\": \"victim@example.com" ++ }, ++ "updateDate" : {"$date" : {"$numberLong" : "1557827033449"}}, ++ "creationDate" : {"$date" : {"$numberLong" : "1557827033449"}}, ++ "keyMaterial" : { ++ "$binary" : { ++ "base64" : "PwwOESbKs57YTJtGSCsuAJbv9VWRHjLdPdziUVxH0K9woZUka4SghSwJlw9n6TU/dYHFODvSa4p7bfKzS6U8kGFvOnd7LcPU63IkTek4qZEbGpTRwI5lMT4FFWdpYpUf", ++ "subType" : "00" ++ } ++ } ++ }); ++ // clang-format on ++ ++ mongocrypt_t *const crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); ++ mongocrypt_ctx_t *const ctx = mongocrypt_ctx_new(crypt); ++ mongocrypt_binary_t *const uuid = mongocrypt_binary_new_from_data((uint8_t *)uuid_data, UUID_LEN); ++ ASSERT_OK(mongocrypt_ctx_setopt_key_id(ctx, uuid), ctx); ++ ASSERT_OK(mongocrypt_ctx_setopt_algorithm(ctx, MONGOCRYPT_ALGORITHM_DETERMINISTIC_STR, -1), ctx); ++ ASSERT_OK(mongocrypt_ctx_explicit_encrypt_init(ctx, TEST_BSON("{'v': 1}")), ctx); ++ ++ ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_MONGO_KEYS); ++ /* Parsing the key document fails. No request is sent. */ ++ ASSERT_FAILS(mongocrypt_ctx_mongo_feed(ctx, TEST_BSON(key_doc)), ctx, "Invalid character in endpoint"); ++ ++ mongocrypt_binary_destroy(uuid); ++ mongocrypt_ctx_destroy(ctx); ++ mongocrypt_destroy(crypt); ++} ++ + void _mongocrypt_tester_install_gcp_auth(_mongocrypt_tester_t *tester) { + INSTALL_TEST(_test_crlf_in_gcp_masterkey); ++ INSTALL_TEST(_test_dot_segment_in_gcp_keyring); ++ INSTALL_TEST(_test_quote_in_gcp_endpoint); + INSTALL_TEST(_test_createdatakey_with_credentials); + INSTALL_TEST(_test_encrypt_with_credentials); + INSTALL_TEST(_test_createdatakey_with_accesstoken); +--- a/test/test-mongocrypt-datakey.c ++++ b/test/test-mongocrypt-datakey.c +@@ -256,8 +256,8 @@ + crypt = mongocrypt_new(); + mongocrypt_setopt_use_need_kms_credentials_state(crypt); + ASSERT_OK(mongocrypt_setopt_kms_providers(crypt, +- TEST_BSON("{'aws': {}, 'azure': {'tenantId': '', 'clientId': " +- "'', 'clientSecret': '' }}")), ++ TEST_BSON("{'aws': {}, 'azure': {'tenantId': 'placeholder-tenantId', " ++ "'clientId': '', 'clientSecret': '' }}")), + crypt); + ASSERT_OK(_mongocrypt_init_for_test(crypt), crypt); + ctx = mongocrypt_ctx_new(crypt); +@@ -565,6 +565,27 @@ + _test_create_data_key_with_provider(tester, MONGOCRYPT_KMS_PROVIDER_LOCAL, true /* with_alt_name */); + } + ++// Test a double quote in masterKey.key. The key identifier is interpolated into the JSON body of the AWS ++// Encrypt request. Regression test for MONGOCRYPT-968. ++static void _test_quote_in_aws_masterkey(_mongocrypt_tester_t *tester) { ++ mongocrypt_t *const crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); ++ mongocrypt_ctx_t *const ctx = mongocrypt_ctx_new(crypt); ++ ++ ASSERT_OK(mongocrypt_ctx_setopt_key_encryption_key( ++ ctx, ++ TEST_BSON(BSON_STR({ ++ "provider" : "aws", ++ "region" : "us-east-1", ++ "key" : "arn\", \"EncryptionContext\" : {\"a\" : \"b\"}, \"KeyId\" : \"attacker-key" ++ }))), ++ ctx); ++ /* Constructing the KMS request fails. No request is sent. */ ++ ASSERT_FAILS(mongocrypt_ctx_datakey_init(ctx), ctx, "Invalid character in AWS KMS key identifier"); ++ ++ mongocrypt_ctx_destroy(ctx); ++ mongocrypt_destroy(crypt); ++} ++ + void _mongocrypt_tester_install_data_key(_mongocrypt_tester_t *tester) { + INSTALL_TEST(_test_random_generator); + INSTALL_TEST(_test_create_data_key); +@@ -574,4 +595,5 @@ + INSTALL_TEST(_test_datakey_kms_per_ctx_credentials_not_requested); + INSTALL_TEST(_test_datakey_kms_per_ctx_credentials_local); + INSTALL_TEST(_test_create_datakey_with_retry); ++ INSTALL_TEST(_test_quote_in_aws_masterkey); + } +--- a/test/test-mongocrypt.c ++++ b/test/test-mongocrypt.c +@@ -550,7 +550,7 @@ + kms_providers = BCON_NEW("azure", + "{", + "tenantId", +- "", ++ "placeholder-tenantId", + "clientId", + "", + "clientSecret", diff -Nru libmongocrypt-1.13.2/debian/patches/series libmongocrypt-1.13.2/debian/patches/series --- libmongocrypt-1.13.2/debian/patches/series 2026-08-29 15:20:33.000000000 +0000 +++ libmongocrypt-1.13.2/debian/patches/series 2026-11-03 20:28:53.000000000 +0000 @@ -1 +1,11 @@ 0001_CVE-2026-81523.patch +0002_CVE-2026-84962.patch +0003_CVE-2026-84971.patch +0004_CVE-2026-106429_1_of_3.patch +0005_CVE-2026-106429_2_of_3.patch +0006_CVE-2026-106429_3_of_3.patch +0007_CVE-2026-106434_1_of_2.patch +0008_CVE-2026-106434_2_of_2.patch +0009_CVE-2026-106433_1_of_2.patch +0010_CVE-2026-106433_2_of_2.patch +0011_CVE-2026-114655.patch