-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Major - P3
-
Affects Version/s: None
-
Component/s: None
-
Server Security
-
Fully Compatible
-
ALL
-
v9.0, v8.3, v8.0, v7.0, v8.2
-
Server Security 2026-07-03, Server Security 2026-07-17, Server Security 2026-07-31, Server Security 2026-08-14, Server Security 2026-08-28, Server Security 2026-09-11, Server Security 2026-10-23, Server Security 2026-11-06, Server Security 2026-11-20
-
None
-
None
-
None
-
None
-
None
-
None
-
None
Security Context
See SECBUG-1278 for problem description, security impact, preconditions, reproduction steps, and severity rationale.
Root Cause
- src/mongo/db/commands/user_management_commands.cpp: the cleanup filters in dropAllRolesFromDBTxnFn are built as nested plain objects that do not match array elements:
- User-doc cleanup uses filter rolesMatch = {roles: {db: X}} — this matches a roles field whose value is the document {"db": X} exactly, not an array element containing db: X
- Role-doc cleanup uses filter {roles: {"roles.db": X}} — likewise malformed for array matching
- Both updates become no-ops while the role documents themselves are deleted, leaving dangling references in admin.system.users and admin.system.roles
- src/mongo/db/commands/user_management_commands.cpp:1889: dropRole (the single-role command) already uses the correct idiom — $elemMatch — confirming the intended pattern; only the bulk command is affected
Proposed Fix
Replace both malformed filters with $elemMatch predicates, matching the idiom already used by dropRole:
// user_management_commands.cpp — in dropAllRolesFromDBTxnFn // Correct filter for user-doc cleanup auto rolesMatch = BSON("roles" << BSON("$elemMatch" << roleMatch)); buildUpdateRequest(usersNSS(...), rolesMatch, BSON("$pull" << roleMatch)); // Correct filter for role-doc cleanup buildUpdateRequest(rolesNSS(...), rolesMatch, BSON("$pull" << roleMatch));
Ensure both cleanup updates run before role document deletion and remain inside the same UMC transaction path.
Acceptance Criteria
- Running dropAllRolesFromDatabase removes all roles array references with db == targetDb from both system.users and system.roles documents.
- Recreating a previously dropped role does not restore access for users/roles unless explicitly re-granted.
- Existing dropRole behavior remains unchanged.
- Regression tests fail on old behavior and pass with the fix.
Test Coverage
- Add/extend unit/integration coverage around dropAllRolesFromDatabase in auth role-management tests.
- Add a regression scenario that:
- grants role r1 to u1
- runs dropAllRolesFromDatabase
- recreates r1 with different privileges
- asserts u1 does not gain recreated-role privileges until explicit grant
- Validate both user-role and role-role reference cleanup.
Notes
The vulnerable filters on master (src/mongo/db/commands/user_management_commands.cpp:1959-1977):
auto roleMatch = BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname.serializeWithoutTenantPrefix_UNSAFE()); auto rolesMatch = BSON("roles" << roleMatch); // users cleanup — filter {roles: {db: X}} does not match array elements buildUpdateRequest(usersNSS(...), rolesMatch, BSON("$pull" << rolesMatch)); // roles cleanup — filter {roles: {"roles.db": X}} is also malformed buildUpdateRequest(rolesNSS(...), BSON("roles" << BSON("roles.db" << dbname.serializeWithoutTenantPrefix_UNSAFE())), BSON("$pull" << rolesMatch));
The correct $elemMatch idiom is already used by dropRole (user_management_commands.cpp:1889):
BSON("roles" << BSON("$elemMatch" << roleName.toBSON()))