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

Reply via email to