Package: release.debian.org
Severity: normal
Tags: trixie
User: [email protected]
Usertags: pu
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
[ Reason ]
This update includes a variety of CVE fixes for vulnerabilities which do
not warrant separate DSAs.
Address the following issues:
* 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
[ Impact ]
Without these fix, users and applications using and integrating
libmongocrypt components may be vulnerable to potential information
modification or disclosure.
[ Tests ]
The affected/changed code went through upstream code reviews. Also,
accompanying unit tests were implemented and executed in upstream's
extensive CI environment.
[ Risks ]
Code changes are minimal (to the extent possible), extensively
reviewed/tested, and low risk. There are no work arounds.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
[ Changes ]
Backport the upstream changes corresponding to the above listed CVEs.
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCgAdFiEEIYZ1DR4ae5UL01q7ldFmTdL1kUIFAmrqYX0ACgkQldFmTdL1
kUJ3RA/+P4UKcf7vYOM9R9Z6dWDUM+iEu/MH76vjf0wPYDrAqaI2f+mWcBfaadrt
7SWXDJS3UzCZCapZ+Dv/D7OcVZk9hH0ToRM3pM6KFwWFZDhlDSU5GOi6IWDJlFCv
CRCWCONfv309GUDcHMVFvp2MxTmaPr3Yvb5sreOKu87Brcdb+oPkXlOqQ7fjfYiS
e4VamwTXTgRIh/kuYHc/ZMpGp/rtghDHY0C7h8THdwZhHZS6qx94aIZhoQdNJQQP
kmkXtrz0myVj7dtyrHF4SSJcjymFoebN12qPYupwx3/gBoxar/LM96mgVH1lwdD9
Dw8qkU5GKSImi+AH/9WWzwJLt9NhXlKmDpA0WkdTrjH+QGDmbsLpQ+phzt7mxvNl
ffw1HH9evp8fpQLJoQUK2Eaqg3gIV7HvexCJbUbHGNSLKzYSdW/+jtq/3UNMJTtO
eKk8Q0LmyzRg4okxiNPuQz1gJBD9dDJyog84AqIpMz9YwL13Euc2ZXrXuJhGlpjb
IpLgWxhho81LiIW3ISCxQ94JyRNXUzz8LWksIzPLUG9ITN2R62QlzHtFXN36jgq1
q92dqDmHSeyULllKmzVLR/mV0NBnWaDX5wF+tOmy4bXXLcnHa1Tss4OKkDkFvaZ2
ONUeZ40Z9quLHOirZrONMEv7+YEHAw5tGkoTSQWcVVgkaiAl30U=
=z8C+
-----END PGP SIGNATURE-----
diff -Nru libmongocrypt-1.13.2/debian/changelog
libmongocrypt-1.13.2/debian/changelog
--- libmongocrypt-1.13.2/debian/changelog 2026-08-29 11:20:33.000000000
-0400
+++ libmongocrypt-1.13.2/debian/changelog 2026-11-03 15:28:53.000000000
-0500
@@ -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 <[email protected]> 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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0002_CVE-2026-84962.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,143 @@
+From 53b74103284cbdf035d97f177a796093c95525f5 Mon Sep 17 00:00:00 2001
+From: Kevin Albertson <[email protected]>
+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 <ctype.h>
++
+ /* 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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0003_CVE-2026-84971.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,281 @@
+From 02a0bb0178270c729a0a11f29b6f637870b287ea Mon Sep 17 00:00:00 2001
+From: mdb-ad <[email protected]>
+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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0004_CVE-2026-106429_1_of_3.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,97 @@
+From f2ea9f97dff40eb8b470a4fc3d407bafb4650d88 Mon Sep 17 00:00:00 2001
+From: Kevin Albertson <[email protected]>
+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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0005_CVE-2026-106429_2_of_3.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,353 @@
+From 1450714452daaaa9da29359ab6a6b1a0c3a1fa47 Mon Sep 17 00:00:00 2001
+From: Kevin Albertson <[email protected]>
+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\": \"[email protected]", /* 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[] = {
++ "[email protected]", /* 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("[email protected]",
-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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0006_CVE-2026-106429_3_of_3.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,22 @@
+From 03d09f07bb32f9387aec823444cd7bb6d41cddad Mon Sep 17 00:00:00 2001
+From: Kevin Albertson <[email protected]>
+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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0007_CVE-2026-106434_1_of_2.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,111 @@
+From 5e68bbf5825b59978e21c595c1af6d39a5e00e94 Mon Sep 17 00:00:00 2001
+From: Connor MacDonald <[email protected]>
+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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0008_CVE-2026-106434_2_of_2.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,194 @@
+From e9d07e8a7d593dde93681943b7070634aebd1980 Mon Sep 17 00:00:00 2001
+From: Connor MacDonald <[email protected]>
+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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0009_CVE-2026-106433_1_of_2.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,71 @@
+From 556bcb70b6efecb7f568d824b03469df3a3bc91a Mon Sep 17 00:00:00 2001
+From: Connor MacDonald <[email protected]>
+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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0010_CVE-2026-106433_2_of_2.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,138 @@
+From 8bc580ea69eea13a932197221edf72830fbc38a1 Mon Sep 17 00:00:00 2001
+From: Connor MacDonald <[email protected]>
+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
1969-12-31 19:00:00.000000000 -0500
+++ libmongocrypt-1.13.2/debian/patches/0011_CVE-2026-114655.patch
2026-11-03 15:28:53.000000000 -0500
@@ -0,0 +1,1169 @@
+From a83b565593e906710a7d61121ac44ffc190eb21c Mon Sep 17 00:00:00 2001
+From: Kevin Albertson <[email protected]>
+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 <ctype.h>
++
++/* _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 <ctype.h>
+-
+ /* 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 <base64url header>.<base64url claims>.<base64url
+ * signature> */
+ 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 <sys/stat.h>
+ #include <time.h>
+ #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",
++ "[email protected]",
++ "https://oauth2.googleapis.com/token",
++ "https://www.example.com\", \"sub\":
\"[email protected]/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\": \"[email protected]"
++ },
++ "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 11:20:33.000000000
-0400
+++ libmongocrypt-1.13.2/debian/patches/series 2026-11-03 15:28:53.000000000
-0500
@@ -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