Skip to content

Commit 1f1a526

Browse files
committed
CSHARP-3521: Redact security sensitive commands and replies.
1 parent ec2dfa9 commit 1f1a526

File tree

3 files changed

+99
-71
lines changed

3 files changed

+99
-71
lines changed

src/MongoDB.Driver.Core/Core/Connections/CommandEventHelper.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
using System;
1717
using System.Collections.Concurrent;
1818
using System.Collections.Generic;
19-
using System.Collections.ObjectModel;
2019
using System.Diagnostics;
2120
using System.IO;
2221
using System.Linq;
@@ -271,7 +270,8 @@ private void ProcessCommandRequestMessage(CommandRequestMessage originalMessage,
271270
var commandName = command.GetElement(0).Name;
272271
var databaseName = command["$db"].AsString;
273272
var databaseNamespace = new DatabaseNamespace(databaseName);
274-
if (ShouldRedactMessage(commandName, command))
273+
var shouldRedactCommand = ShouldRedactCommand(command);
274+
if (shouldRedactCommand)
275275
{
276276
command = new BsonDocument();
277277
}
@@ -298,7 +298,8 @@ private void ProcessCommandRequestMessage(CommandRequestMessage originalMessage,
298298
OperationId = operationId,
299299
Stopwatch = stopwatch,
300300
QueryNamespace = new CollectionNamespace(databaseNamespace, "$cmd"),
301-
ExpectedResponseType = decodedMessage.MoreToCome ? ExpectedResponseType.None : ExpectedResponseType.Command
301+
ExpectedResponseType = decodedMessage.MoreToCome ? ExpectedResponseType.None : ExpectedResponseType.Command,
302+
ShouldRedactReply = shouldRedactCommand
302303
});
303304
}
304305
}
@@ -318,7 +319,7 @@ private void ProcessCommandResponseMessage(CommandState state, CommandResponseMe
318319
return;
319320
}
320321

321-
if (ShouldRedactMessage(state.CommandName, reply))
322+
if (state.ShouldRedactReply)
322323
{
323324
reply = new BsonDocument();
324325
}
@@ -589,12 +590,14 @@ private void ProcessQueryMessage(QueryMessage originalMessage, ConnectionId conn
589590
var isCommand = IsCommand(decodedMessage.CollectionNamespace);
590591
string commandName;
591592
BsonDocument command;
593+
var shouldRedactCommand = false;
592594
if (isCommand)
593595
{
594596
command = decodedMessage.Query;
595597
var firstElement = command.GetElement(0);
596598
commandName = firstElement.Name;
597-
if (ShouldRedactMessage(commandName, command))
599+
shouldRedactCommand = ShouldRedactCommand(command);
600+
if (shouldRedactCommand)
598601
{
599602
command = new BsonDocument();
600603
}
@@ -631,7 +634,8 @@ private void ProcessQueryMessage(QueryMessage originalMessage, ConnectionId conn
631634
OperationId = operationId,
632635
Stopwatch = stopwatch,
633636
QueryNamespace = decodedMessage.CollectionNamespace,
634-
ExpectedResponseType = isCommand ? ExpectedResponseType.Command : ExpectedResponseType.Query
637+
ExpectedResponseType = isCommand ? ExpectedResponseType.Command : ExpectedResponseType.Query,
638+
ShouldRedactReply = shouldRedactCommand
635639
});
636640
}
637641
}
@@ -675,7 +679,7 @@ private void ProcessReplyMessage(CommandState state, ResponseMessage message, IB
675679
(state.ExpectedResponseType != ExpectedResponseType.Query && replyMessage.Documents.Count == 0))
676680
{
677681
var queryFailureDocument = replyMessage.QueryFailureDocument;
678-
if (ShouldRedactMessage(state.CommandName, queryFailureDocument))
682+
if (state.ShouldRedactReply)
679683
{
680684
queryFailureDocument = new BsonDocument();
681685
}
@@ -730,7 +734,7 @@ private void ProcessCommandReplyMessage(CommandState state, ReplyMessage<RawBson
730734
return;
731735
}
732736

733-
if (ShouldRedactMessage(state.CommandName, reply))
737+
if (state.ShouldRedactReply)
734738
{
735739
reply = new BsonDocument();
736740
}
@@ -1088,23 +1092,25 @@ private static bool IsCommand(CollectionNamespace collectionNamespace)
10881092
return collectionNamespace.Equals(collectionNamespace.DatabaseNamespace.CommandCollection);
10891093
}
10901094

1091-
private static bool ShouldRedactMessage(string commandName, BsonDocument command)
1095+
private static bool ShouldRedactCommand(BsonDocument command)
10921096
{
1097+
var commandName = command.GetElement(0).Name;
10931098
switch (commandName.ToLowerInvariant())
10941099
{
1100+
// string constants MUST all be lowercase for the case-insensitive comparison to work
10951101
case "authenticate":
1096-
case "saslStart":
1097-
case "saslContinue":
1102+
case "saslstart":
1103+
case "saslcontinue":
10981104
case "getnonce":
1099-
case "createUser":
1100-
case "updateUser":
1105+
case "createuser":
1106+
case "updateuser":
11011107
case "copydbgetnonce":
11021108
case "copydbsaslstart":
11031109
case "copydb":
11041110
return true;
11051111

1106-
case "isMaster":
1107-
return command.Contains("speculativeAuthenticate");
1112+
case "ismaster":
1113+
return command.Names.Any(n => n.ToLowerInvariant() == "speculativeauthenticate");
11081114

11091115
default:
11101116
return false;
@@ -1129,6 +1135,7 @@ private class CommandState
11291135
public ExpectedResponseType ExpectedResponseType;
11301136
public BsonDocument NoResponseResponse;
11311137
public BsonValue UpsertedId;
1138+
public bool ShouldRedactReply;
11321139
}
11331140
}
11341141
}

tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnection_CommandEventTests.cs

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,25 @@ public class BinaryConnection_CommandEventTests : IDisposable
5050

5151
public static IEnumerable<object[]> GetPotentiallyRedactedCommandTestCases()
5252
{
53-
var potentiallyRedactedCommands = new[]
53+
return new object[][]
5454
{
55-
"authenticate",
56-
"saslStart",
57-
"saslContinue",
58-
"getnonce",
59-
"createUser",
60-
"updateUser",
61-
"copydbgetnonce",
62-
"copydbsaslstart",
63-
"copydb",
64-
"isMaster",
55+
// string commandJson, bool shouldBeRedacted
56+
new object[] { "{ xyz : 1 }", false },
57+
new object[] { "{ authenticate : 1 }", true },
58+
new object[] { "{ saslStart : 1 }", true },
59+
new object[] { "{ saslContinue : 1 }", true },
60+
new object[] { "{ getnonce : 1 }", true },
61+
new object[] { "{ createUser : 1 }", true },
62+
new object[] { "{ updateUser : 1 }", true },
63+
new object[] { "{ copydbgetnonce : 1 }", true },
64+
new object[] { "{ copydbsaslstart : 1 }", true },
65+
new object[] { "{ copydb : 1 }", true },
66+
new object[] { "{ authenticate : 1 }", true },
67+
new object[] { "{ isMaster : 1 }", false },
68+
new object[] { "{ isMaster : 1, speculativeAuthenticate : { } }", true },
6569
};
66-
return potentiallyRedactedCommands.Select(c => new object[] { c });
6770
}
71+
6872
public BinaryConnection_CommandEventTests()
6973
{
7074
_capturedEvents = new EventCapturer()
@@ -144,13 +148,10 @@ public void Should_process_a_command()
144148

145149
[Theory]
146150
[MemberData(nameof(GetPotentiallyRedactedCommandTestCases))]
147-
public void Should_process_a_redacted_command(string commandName)
151+
public void Should_process_a_redacted_command(string commandJson, bool shouldBeRedacted)
148152
{
149-
var command = BsonDocument.Parse($"{{ {commandName}: 1, extra: true }}");
150-
command = ModifyMessageToTriggerConditionToRedact(commandName, command);
151-
153+
var command = BsonDocument.Parse(commandJson);
152154
var reply = BsonDocument.Parse("{ ok: 1, extra: true }");
153-
reply = ModifyMessageToTriggerConditionToRedact(commandName, reply);
154155

155156
var requestMessage = MessageHelper.BuildCommand(
156157
command,
@@ -163,17 +164,11 @@ public void Should_process_a_redacted_command(string commandName)
163164
responseTo: requestMessage.RequestId);
164165
ReceiveMessages(replyMessage);
165166

166-
167-
var commandStartedCommandShouldBeRedacted = CommandEventHelperReflector.ShouldRedactMessage(commandName, command);
168-
var commandSucceededCommandShouldBeRedacted = CommandEventHelperReflector.ShouldRedactMessage(commandName, replyMessage.Documents[0]);
169-
var commandStartedCommandExpectedElementCount = commandStartedCommandShouldBeRedacted ? 0 : command.ElementCount;
170-
var commandSucceededCommandExpectedElementCount = commandSucceededCommandShouldBeRedacted ? 0 : reply.ElementCount;
171-
172167
var commandStartedEvent = (CommandStartedEvent)_capturedEvents.Next();
173168
var commandSucceededEvent = (CommandSucceededEvent)_capturedEvents.Next();
174169

175170
commandStartedEvent.CommandName.Should().Be(command.GetElement(0).Name);
176-
commandStartedEvent.Command.ElementCount.Should().Be(commandStartedCommandExpectedElementCount);
171+
commandStartedEvent.Command.Should().Be(shouldBeRedacted ? new BsonDocument() : command);
177172
commandStartedEvent.ConnectionId.Should().Be(_subject.ConnectionId);
178173
commandStartedEvent.DatabaseNamespace.Should().Be(MessageHelper.DefaultDatabaseNamespace);
179174
commandStartedEvent.OperationId.Should().Be(EventContext.OperationId);
@@ -183,7 +178,7 @@ public void Should_process_a_redacted_command(string commandName)
183178
commandSucceededEvent.ConnectionId.Should().Be(commandStartedEvent.ConnectionId);
184179
commandSucceededEvent.Duration.Should().BeGreaterThan(TimeSpan.Zero);
185180
commandSucceededEvent.OperationId.Should().Be(commandStartedEvent.OperationId);
186-
commandSucceededEvent.Reply.ElementCount.Should().Be(commandSucceededCommandExpectedElementCount);
181+
commandSucceededEvent.Reply.Should().Be(shouldBeRedacted ? new BsonDocument() : reply);
187182
commandSucceededEvent.RequestId.Should().Be(commandStartedEvent.RequestId);
188183
}
189184

@@ -224,13 +219,10 @@ public void Should_process_a_failed_command()
224219

225220
[Theory]
226221
[MemberData(nameof(GetPotentiallyRedactedCommandTestCases))]
227-
public void Should_process_a_redacted_failed_command(string commandName)
222+
public void Should_process_a_redacted_failed_command(string commandJson, bool shouldBeRedacted)
228223
{
229-
var command = BsonDocument.Parse($"{{ {commandName}: 1, extra: true }}");
230-
command = ModifyMessageToTriggerConditionToRedact(commandName, command);
231-
224+
var command = BsonDocument.Parse(commandJson);
232225
var reply = BsonDocument.Parse("{ ok: 0, extra: true }");
233-
reply = ModifyMessageToTriggerConditionToRedact(commandName, reply);
234226

235227
var requestMessage = MessageHelper.BuildCommand(
236228
command,
@@ -243,16 +235,11 @@ public void Should_process_a_redacted_failed_command(string commandName)
243235
responseTo: requestMessage.RequestId);
244236
ReceiveMessages(replyMessage);
245237

246-
var commandStartedCommandShouldBeRedacted = CommandEventHelperReflector.ShouldRedactMessage(commandName, command);
247-
var commandSucceededCommandShouldBeRedacted = CommandEventHelperReflector.ShouldRedactMessage(commandName, replyMessage.Documents[0]);
248-
var commandStartedCommandExpectedElementCount = commandStartedCommandShouldBeRedacted ? 0 : command.ElementCount;
249-
var commandSucceededCommandExpectedElementCount = commandSucceededCommandShouldBeRedacted ? 0 : reply.ElementCount;
250-
251238
var commandStartedEvent = (CommandStartedEvent)_capturedEvents.Next();
252239
var commandFailedEvent = (CommandFailedEvent)_capturedEvents.Next();
253240

254241
commandStartedEvent.CommandName.Should().Be(command.GetElement(0).Name);
255-
commandStartedEvent.Command.ElementCount.Should().Be(commandStartedCommandExpectedElementCount);
242+
commandStartedEvent.Command.Should().Be(shouldBeRedacted ? new BsonDocument() : command);
256243
commandStartedEvent.ConnectionId.Should().Be(_subject.ConnectionId);
257244
commandStartedEvent.DatabaseNamespace.Should().Be(MessageHelper.DefaultDatabaseNamespace);
258245
commandStartedEvent.OperationId.Should().Be(EventContext.OperationId);
@@ -265,7 +252,7 @@ public void Should_process_a_redacted_failed_command(string commandName)
265252
commandFailedEvent.RequestId.Should().Be(commandStartedEvent.RequestId);
266253
commandFailedEvent.Failure.Should().BeOfType<MongoCommandException>();
267254
var exception = (MongoCommandException)commandFailedEvent.Failure;
268-
exception.Result.ElementCount.Should().Be(commandSucceededCommandExpectedElementCount);
255+
exception.Result.Should().Be(shouldBeRedacted ? new BsonDocument() : reply);
269256
}
270257

271258
[Fact]
@@ -982,24 +969,5 @@ private void ReceiveMessages(params ReplyMessage<BsonDocument>[] messages)
982969
_subject.ReceiveMessageAsync(message.ResponseTo, encoderSelector, _messageEncoderSettings, CancellationToken.None).Wait();
983970
}
984971
}
985-
986-
private static BsonDocument ModifyMessageToTriggerConditionToRedact(string commandName, BsonDocument command)
987-
{
988-
switch (commandName)
989-
{
990-
case "isMaster":
991-
command.Add("speculativeAuthenticate", new BsonDocument("db", "authSource"));
992-
break;
993-
}
994-
995-
return command;
996-
}
997-
}
998-
999-
internal static class CommandEventHelperReflector
1000-
{
1001-
public static bool ShouldRedactMessage(string commandName, BsonDocument command) =>
1002-
(bool)Reflector.InvokeStatic(typeof(CommandEventHelper), nameof(ShouldRedactMessage), commandName, command);
1003-
1004972
}
1005973
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using FluentAssertions;
17+
using MongoDB.Bson;
18+
using MongoDB.Bson.TestHelpers;
19+
using Xunit;
20+
21+
namespace MongoDB.Driver.Core.Connections
22+
{
23+
public class CommandEventHelperTests
24+
{
25+
[Theory]
26+
[InlineData("{ xyz : 1 }", false)]
27+
[InlineData("{ aUTHENTICATE: 1 }", true)]
28+
[InlineData("{ sASLSTART : 1 }", true)]
29+
[InlineData("{ sASLCONTINUE : 1 }", true)]
30+
[InlineData("{ gETNONCE : 1 }", true)]
31+
[InlineData("{ cREATEUSER : 1 }", true)]
32+
[InlineData("{ uPDATEUSER : 1, }", true)]
33+
[InlineData("{ cOPYDBGETNONCE : 1 }", true)]
34+
[InlineData("{ cOPYDBSASLSTART : 1 }", true)]
35+
[InlineData("{ cOPYDB : 1 }", true)]
36+
[InlineData("{ iSMASTER : 1 }", false)]
37+
[InlineData("{ iSMASTER : 1, sPECULATIVEAUTHENTICATE : null }", true)]
38+
public void ShouldRedactCommand_should_return_expected_result(string commandJson, bool expectedResult)
39+
{
40+
var command = BsonDocument.Parse(commandJson);
41+
42+
var result = CommandEventHelperReflector.ShouldRedactCommand(command);
43+
44+
result.Should().Be(expectedResult);
45+
}
46+
}
47+
48+
public static class CommandEventHelperReflector
49+
{
50+
public static bool ShouldRedactCommand(BsonDocument command) =>
51+
(bool)Reflector.InvokeStatic(typeof(CommandEventHelper), nameof(ShouldRedactCommand), command);
52+
}
53+
}

0 commit comments

Comments
 (0)