-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Major - P3
-
Affects Version/s: None
-
Component/s: Queryable Encryption
-
Server Security
-
Fully Compatible
-
ALL
-
v9.0, v8.3, v8.0, v7.0, v8.2
-
Server Security 2026-07-17, Server Security 2026-07-31, Server Security 2026-08-28, Server Security 2026-12-04
-
200
-
None
-
None
-
None
-
None
-
None
-
None
-
None
Security Context
See SECBUG-1031 for problem description, security impact, preconditions, reproduction steps, and severity rationale.
Root Cause
Authorization is checked only on the user-facing EDC namespace; the internal state collection names supplied by the client in encryptionInformation are never validated against the EDC collection or authorized separately.
Gap 1 — isFLE2StateCollection validates format only, not derivation
src/mongo/crypto/encryption_fields_validation.cpp:672-680 calls NamespaceString::isFLE2StateCollection() on the client-supplied escCollection and ecocCollection values. That function (namespace_string.cpp:327-336) checks only that the name starts with enxcol_. and ends with .esc or .ecoc. It does not verify that the middle segment matches the EDC collection name. Any enxcol_.*.esc string passes.
Gap 2 — Authorization checks only the EDC namespace
src/mongo/db/commands/query_cmd/write_commands_common.cpp:88-94 builds privileges solely from op.getNamespace() (the EDC collection) and never touches the escCollection or ecocCollection fields. An attacker naming a different collection's state tables in encryptionInformation adds no required privilege.
Gap 3 — State collection writes execute as _system
src/mongo/db/fle_crud.cpp:867/955/959/998 constructs nssEsc and nssEcoc directly from the client-supplied names (using only edcNss.dbName() for the database). Writes are dispatched via SEPTransactionClient, which calls primeInternalClient() → grantInternalAuthorization() (sep_transaction_client.cpp:162-197), granting __system for the inner transaction. The insert into the attacker-named state collection succeeds regardless of the caller's grants.
The combined effect: a user with insert on any collection in a database can name any other encrypted collection's state tables in encryptionInformation.schema, and the server will write attacker-shaped ESC and ECOC documents into those tables under _system authority.
Proposed Fix
Cross-validate the client-supplied state collection names against the EDC namespace in getAndValidateSchema or validateEncryptedFieldConfig, before any insert processing begins:
// In getAndValidateSchema (fle_crypto.cpp) or validateEncryptedFieldConfig // (encryption_fields_validation.cpp), after parsing efc from the client request:auto expectedEsc = "enxcol_." + edcCollName + ".esc"; auto expectedEcoc = "enxcol_." + edcCollName + ".ecoc";uassert(ErrorCodes::BadValue, str::stream() << "escCollection must be '" << expectedEsc << "'", config->getEscCollection().value_or("") == expectedEsc);uassert(ErrorCodes::BadValue, str::stream() << "ecocCollection must be '" << expectedEcoc << "'", config->getEcocCollection().value_or("") == expectedEcoc);
This rejects any request where the client has redirected state collection names to a different collection, before reaching fle_crud.cpp. Legitimate drivers always derive the state collection names from the EDC name, so no valid client is affected.
Acceptance Criteria
- An insert with encryptionInformation.schema naming a state collection that does not derive from the EDC collection name returns BadValue before any write occurs.
- A normal QE insert where escCollection and ecocCollection match the EDC collection name proceeds without change.
- The fix applies to insert, update, and delete paths (all callers of getAndValidateSchema).
Test Coverage
- Unit test: construct an EncryptedFieldConfig with escCollection set to a different collection's state table name; call validateEncryptedFieldConfig or getAndValidateSchema and assert BadValue.
- Integration test: reproduce the SECBUG-1031 trigger (insert with redirected encryptionInformation); assert the insert is rejected and the victim collection's state tables are unmodified.
Worth Fixing Assessment
Yes — as a CVE backported to all branches, not a deferred maintenance item.
The CVSS 4.0 score of 2.3 is driven down by AT
(requires a second QE-encrypted collection in the same database). In a production QE deployment that condition is normal, not unusual, making the practical attack bar closer to AT:N and the effective score closer to 4.8. More importantly, CVSS score should not be the primary signal here: this is an authorization bypass in a subsystem whose entire purpose is protecting sensitive data. The principle is simple — a user with insert on collection A must never be able to write into collection B's metadata — and the current code violates it.
The fix is two uassert calls in a single function. Engineering cost is lower than any other CVE in this batch. The risk of regression is negligible because legitimate drivers always derive state collection names from the EDC name; no valid client supplies a different value. Given the low fix cost and the qualitative weight of an authz bypass in a security subsystem, there is no reasonable case for deferring this.
Notes
The redirect path on master (fle_crud.cpp):
// processFieldsForInsert — nssEsc built from attacker-supplied name, not from edcNss.coll() const NamespaceString nssEsc = NamespaceStringUtil::deserialize(edcNss.dbName(), efc.getEscCollection().value()); // ... uassertStatusOK(queryImpl->insertDocuments(nssEsc, escDocuments, pStmtId, true)); // nssEcoc same pattern at lines 955/959/998
The _system escalation path (sep_transaction_client.cpp:162-197):
// SEPTransactionClient grants internalSecurity before running the inner command — // the caller's original grants are irrelevant for the inner insert primeInternalClient(client); // → grantInternalAuthorization()