diff --git a/src/mc-schema-broker.c b/src/mc-schema-broker.c index be9586a64..e589fdd2a 100644 --- a/src/mc-schema-broker.c +++ b/src/mc-schema-broker.c @@ -323,7 +323,11 @@ bool mc_schema_broker_satisfy_from_collinfo(mc_schema_broker_t *sb, CLIENT_ERR("failed to find 'name' in collinfo in database: %s", sb->db); return false; } - coll = bson_iter_utf8(&name_iter, NULL); + uint32_t coll_len; + coll = bson_iter_utf8(&name_iter, &coll_len); + if (!_mongocrypt_check_no_embedded_nul(coll, coll_len, "collection name in collinfo", status)) { + return false; + } } // Cache the received collinfo. @@ -508,7 +512,11 @@ bool mc_schema_broker_satisfy_from_create_or_collMod(mc_schema_broker_t *sb, CLIENT_ERR("Failed to get collection name from command"); return false; } - const char *coll = bson_iter_utf8(&iter, NULL); + uint32_t coll_len; + const char *coll = bson_iter_utf8(&iter, &coll_len); + if (!_mongocrypt_check_no_embedded_nul(coll, coll_len, "collection name", status)) { + return false; + } // Check if schema was requested. mc_schema_entry_t *found = NULL; diff --git a/src/mongocrypt-ctx-encrypt.c b/src/mongocrypt-ctx-encrypt.c index 766ca1b5e..73fb737fa 100644 --- a/src/mongocrypt-ctx-encrypt.c +++ b/src/mongocrypt-ctx-encrypt.c @@ -2303,7 +2303,11 @@ static bool _check_cmd_for_auto_encrypt_bulkWrite(mongocrypt_binary_t *cmd, return false; } - const char *target_ns = bson_iter_utf8(&ns_iter, NULL /* length */); + uint32_t target_ns_len; + const char *target_ns = bson_iter_utf8(&ns_iter, &target_ns_len); + if (!_mongocrypt_check_no_embedded_nul(target_ns, target_ns_len, "namespace in `bulkWrite` command", status)) { + return false; + } // Parse `target_ns` into "." const char *dot = strstr(target_ns, "."); if (!dot) { @@ -2375,7 +2379,12 @@ _check_cmd_for_auto_encrypt(mongocrypt_binary_t *cmd, bool *bypass, char **targe } if (BSON_ITER_HOLDS_UTF8(&target_coll_iter)) { - *target_coll = bson_strdup(bson_iter_utf8(&target_coll_iter, NULL)); + uint32_t target_coll_len; + const char *target_coll_str = bson_iter_utf8(&target_coll_iter, &target_coll_len); + if (!_mongocrypt_check_no_embedded_nul(target_coll_str, target_coll_len, "collection name", status)) { + return false; + } + *target_coll = bson_strdup(target_coll_str); } else { *target_coll = NULL; } @@ -2572,7 +2581,11 @@ static bool find_collections_in_pipeline(mc_schema_broker_t *sb, stage_key); return false; } - const char *from = bson_iter_utf8(&lookup_iter, NULL); + uint32_t from_len; + const char *from = bson_iter_utf8(&lookup_iter, &from_len); + if (!_mongocrypt_check_no_embedded_nul(from, from_len, "'from' collection name", status)) { + return false; + } if (!mc_schema_broker_request(sb, db, from, status)) { return false; } @@ -2631,7 +2644,11 @@ static bool find_collections_in_pipeline(mc_schema_broker_t *sb, stage_key); return false; } - const char *coll = bson_iter_utf8(&unionWith_iter, NULL); + uint32_t coll_len; + const char *coll = bson_iter_utf8(&unionWith_iter, &coll_len); + if (!_mongocrypt_check_no_embedded_nul(coll, coll_len, "'coll' collection name", status)) { + return false; + } if (!mc_schema_broker_request(sb, db, coll, status)) { return false; } @@ -2722,6 +2739,10 @@ bool mongocrypt_ctx_encrypt_init(mongocrypt_ctx_t *ctx, const char *db, int32_t return _mongocrypt_ctx_fail_w_msg(ctx, "invalid db"); } + if (strchr(ectx->cmd_db, '.')) { + return _mongocrypt_ctx_fail_w_msg(ctx, "invalid db: must not contain a dot"); + } + if (0 == strcmp(ectx->cmd_name, "bulkWrite")) { // Handle `bulkWrite` as a special case. // `bulkWrite` includes the target namespaces in an `nsInfo` field. diff --git a/src/mongocrypt-private.h b/src/mongocrypt-private.h index b1717fbab..cb541e2fa 100644 --- a/src/mongocrypt-private.h +++ b/src/mongocrypt-private.h @@ -161,6 +161,11 @@ typedef enum { bool _mongocrypt_validate_and_copy_string(const char *in, int32_t in_len, char **out) MONGOCRYPT_WARN_UNUSED_RESULT; +/* _mongocrypt_check_no_embedded_nul returns false and sets @status if @str contains an embedded null byte. See + * MONGOCRYPT-977. */ +bool _mongocrypt_check_no_embedded_nul(const char *str, uint32_t len, const char *what, mongocrypt_status_t *status) + MONGOCRYPT_WARN_UNUSED_RESULT; + char *_mongocrypt_new_string_from_bytes(const void *in, int len); /* _mongocrypt_needs_credentials returns true if @crypt was configured to diff --git a/src/mongocrypt.c b/src/mongocrypt.c index 810b58199..91c5de1e6 100644 --- a/src/mongocrypt.c +++ b/src/mongocrypt.c @@ -913,6 +913,17 @@ uint64_t mongocrypt_crypt_shared_lib_version(const mongocrypt_t *crypt) { return crypt->csfle.get_version(); } +bool _mongocrypt_check_no_embedded_nul(const char *str, uint32_t len, const char *what, mongocrypt_status_t *status) { + BSON_ASSERT_PARAM(str); + BSON_ASSERT_PARAM(what); + + if (strlen(str) != (size_t)len) { + CLIENT_ERR("%s must not contain an embedded null byte", what); + return false; + } + return true; +} + bool _mongocrypt_validate_and_copy_string(const char *in, int32_t in_len, char **out) { BSON_ASSERT_PARAM(out); diff --git a/test/test-mongocrypt-ctx-encrypt.c b/test/test-mongocrypt-ctx-encrypt.c index 34b96aecf..a5817d27c 100644 --- a/test/test-mongocrypt-ctx-encrypt.c +++ b/test/test-mongocrypt-ctx-encrypt.c @@ -362,6 +362,119 @@ static void _test_encrypt_init(_mongocrypt_tester_t *tester) { mongocrypt_destroy(crypt); } +/* Test that a db name containing a dot or an embedded NUL is rejected. + * A db name like "a.b" would otherwise produce the namespace "a.b.coll", + * retargeting the operation at database "a" and collection "b.coll". + * Regression test for MONGOCRYPT-977. */ +static void _test_encrypt_init_invalid_db_collection(_mongocrypt_tester_t *tester) { + mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT); + + { + /* Dot in db name. */ + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); + ASSERT_FAILS(mongocrypt_ctx_encrypt_init(ctx, "a.b", -1, TEST_FILE("./test/example/cmd.json")), + ctx, + "invalid db"); + mongocrypt_ctx_destroy(ctx); + } + + { + /* Embedded NUL in db name. */ + const char db[] = "a\0b"; + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); + ASSERT_FAILS(mongocrypt_ctx_encrypt_init(ctx, db, 3, TEST_FILE("./test/example/cmd.json")), ctx, "invalid db"); + mongocrypt_ctx_destroy(ctx); + } + + { + /* A dot in a collection name is legal, and must keep working. */ + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); + ASSERT_OK(mongocrypt_ctx_encrypt_init(ctx, "test", -1, TEST_BSON("{'find': 'a.b'}")), ctx); + mongocrypt_ctx_destroy(ctx); + } + + { + /* Embedded NUL in the collection name of the command. */ + bson_t *cmd = bson_new(); + BSON_ASSERT(bson_append_utf8(cmd, "find", -1, "a\0b", 3)); + mongocrypt_binary_t *bin = mongocrypt_binary_new_from_data((uint8_t *)bson_get_data(cmd), cmd->len); + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); + ASSERT_FAILS(mongocrypt_ctx_encrypt_init(ctx, "test", -1, bin), + ctx, + "collection name must not contain an embedded null byte"); + mongocrypt_ctx_destroy(ctx); + mongocrypt_binary_destroy(bin); + bson_destroy(cmd); + } + + { + /* Embedded NUL in the `bulkWrite` nsInfo namespace. */ + bson_t *cmd = bson_new(); + bson_array_builder_t *nsInfo; + bson_t ns0; + BSON_ASSERT(BSON_APPEND_INT32(cmd, "bulkWrite", 1)); + BSON_ASSERT(BSON_APPEND_ARRAY_BUILDER_BEGIN(cmd, "nsInfo", &nsInfo)); + BSON_ASSERT(bson_array_builder_append_document_begin(nsInfo, &ns0)); + BSON_ASSERT(bson_append_utf8(&ns0, "ns", -1, "db.coll\0evil", 12)); + BSON_ASSERT(bson_array_builder_append_document_end(nsInfo, &ns0)); + BSON_ASSERT(bson_append_array_builder_end(cmd, nsInfo)); + mongocrypt_binary_t *bin = mongocrypt_binary_new_from_data((uint8_t *)bson_get_data(cmd), cmd->len); + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); + ASSERT_FAILS(mongocrypt_ctx_encrypt_init(ctx, "test", -1, bin), ctx, "must not contain an embedded null byte"); + mongocrypt_ctx_destroy(ctx); + mongocrypt_binary_destroy(bin); + bson_destroy(cmd); + } + + { + /* Embedded NUL in a $lookup 'from' collection name. */ + bson_t *cmd = bson_new(); + bson_array_builder_t *pipeline; + bson_t stage, lookup; + BSON_ASSERT(BSON_APPEND_UTF8(cmd, "aggregate", "coll")); + BSON_ASSERT(BSON_APPEND_ARRAY_BUILDER_BEGIN(cmd, "pipeline", &pipeline)); + BSON_ASSERT(bson_array_builder_append_document_begin(pipeline, &stage)); + BSON_ASSERT(BSON_APPEND_DOCUMENT_BEGIN(&stage, "$lookup", &lookup)); + BSON_ASSERT(bson_append_utf8(&lookup, "from", -1, "a\0b", 3)); + BSON_ASSERT(bson_append_document_end(&stage, &lookup)); + BSON_ASSERT(bson_array_builder_append_document_end(pipeline, &stage)); + BSON_ASSERT(bson_append_array_builder_end(cmd, pipeline)); + mongocrypt_binary_t *bin = mongocrypt_binary_new_from_data((uint8_t *)bson_get_data(cmd), cmd->len); + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); + ASSERT_FAILS(mongocrypt_ctx_encrypt_init(ctx, "test", -1, bin), + ctx, + "'from' collection name must not contain an embedded null byte"); + mongocrypt_ctx_destroy(ctx); + mongocrypt_binary_destroy(bin); + bson_destroy(cmd); + } + + { + /* Embedded NUL in a $unionWith 'coll' collection name. */ + bson_t *cmd = bson_new(); + bson_array_builder_t *pipeline; + bson_t stage, unionWith; + BSON_ASSERT(BSON_APPEND_UTF8(cmd, "aggregate", "coll")); + BSON_ASSERT(BSON_APPEND_ARRAY_BUILDER_BEGIN(cmd, "pipeline", &pipeline)); + BSON_ASSERT(bson_array_builder_append_document_begin(pipeline, &stage)); + BSON_ASSERT(BSON_APPEND_DOCUMENT_BEGIN(&stage, "$unionWith", &unionWith)); + BSON_ASSERT(bson_append_utf8(&unionWith, "coll", -1, "a\0b", 3)); + BSON_ASSERT(bson_append_document_end(&stage, &unionWith)); + BSON_ASSERT(bson_array_builder_append_document_end(pipeline, &stage)); + BSON_ASSERT(bson_append_array_builder_end(cmd, pipeline)); + mongocrypt_binary_t *bin = mongocrypt_binary_new_from_data((uint8_t *)bson_get_data(cmd), cmd->len); + mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt); + ASSERT_FAILS(mongocrypt_ctx_encrypt_init(ctx, "test", -1, bin), + ctx, + "'coll' collection name must not contain an embedded null byte"); + mongocrypt_ctx_destroy(ctx); + mongocrypt_binary_destroy(bin); + bson_destroy(cmd); + } + + mongocrypt_destroy(crypt); +} + static void _test_encrypt_need_collinfo(_mongocrypt_tester_t *tester) { mongocrypt_t *crypt; mongocrypt_ctx_t *ctx; @@ -6830,6 +6943,7 @@ static void _test_qe_keyAltName_kms(_mongocrypt_tester_t *tester) { void _mongocrypt_tester_install_ctx_encrypt(_mongocrypt_tester_t *tester) { INSTALL_TEST(_test_explicit_encrypt_init); INSTALL_TEST(_test_encrypt_init); + INSTALL_TEST(_test_encrypt_init_invalid_db_collection); INSTALL_TEST(_test_encrypt_need_collinfo); INSTALL_TEST(_test_encrypt_need_markings); INSTALL_TEST(_test_encrypt_csfle_no_needs_markings);