-
Notifications
You must be signed in to change notification settings - Fork 318
Fix Mongo Query Sanitizing #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,14 +2,21 @@ | |
|
|
||
| import com.datadoghq.trace.DDTags; | ||
| import com.mongodb.MongoClientOptions; | ||
| import com.mongodb.event.CommandFailedEvent; | ||
| import com.mongodb.event.CommandListener; | ||
| import com.mongodb.event.CommandStartedEvent; | ||
| import com.mongodb.event.CommandSucceededEvent; | ||
| import io.opentracing.Span; | ||
| import io.opentracing.contrib.mongo.TracingCommandListener; | ||
| import io.opentracing.contrib.mongo.TracingCommandListenerFactory; | ||
| import io.opentracing.Tracer; | ||
| import io.opentracing.tag.Tags; | ||
| import java.net.Inet4Address; | ||
| import java.net.InetAddress; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.bson.BsonArray; | ||
| import org.bson.BsonDocument; | ||
|
|
@@ -21,10 +28,6 @@ | |
| @Slf4j | ||
| public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder> { | ||
|
|
||
| private static final List<String> WHILDCARD_FIELDS = | ||
| Arrays.asList("ordered", "insert", "count", "find"); | ||
| private static final BsonValue HIDDEN_CAR = new BsonString("?"); | ||
|
|
||
| public MongoHelper(final Rule rule) { | ||
| super(rule); | ||
| } | ||
|
|
@@ -42,62 +45,137 @@ public MongoHelper(final Rule rule) { | |
| protected MongoClientOptions.Builder doPatch(final MongoClientOptions.Builder builder) | ||
| throws Exception { | ||
|
|
||
| final TracingCommandListener listener = TracingCommandListenerFactory.create(tracer); | ||
| final DDTracingCommandListener listener = new DDTracingCommandListener(tracer); | ||
| builder.addCommandListener(listener); | ||
|
|
||
| setState(builder, 1); | ||
|
|
||
| return builder; | ||
| } | ||
|
|
||
| public void decorate(final Span span, final CommandStartedEvent event) { | ||
| try { | ||
| // normalize the Mongo command so that parameters are removed from the string | ||
| final BsonDocument normalized = norm(event.getCommand()); | ||
| final String mongoCmd = normalized.toString(); | ||
| public static class DDTracingCommandListener implements CommandListener { | ||
| /** | ||
| * The values of these mongo fields will not be scrubbed out. This allows the non-sensitive | ||
| * collection names to be captured. | ||
| */ | ||
| private static final List<String> UNSCRUBBED_FIELDS = | ||
| Arrays.asList("ordered", "insert", "count", "find", "create"); | ||
|
|
||
| private static final BsonValue HIDDEN_CHAR = new BsonString("?"); | ||
| private static final String MONGO_OPERATION = "mongo.query"; | ||
|
|
||
| static final String COMPONENT_NAME = "java-mongo"; | ||
| private final Tracer tracer; | ||
| /** Cache for (request id, span) pairs */ | ||
| private final Map<Integer, Span> cache = new ConcurrentHashMap<>(); | ||
|
|
||
| public DDTracingCommandListener(Tracer tracer) { | ||
| this.tracer = tracer; | ||
| } | ||
|
|
||
| // add specific resource name and replace the `db.statement` OpenTracing | ||
| // tag with the quantized version of the Mongo command | ||
| @Override | ||
| public void commandStarted(CommandStartedEvent event) { | ||
| Span span = buildSpan(event); | ||
| cache.put(event.getRequestId(), span); | ||
| } | ||
|
|
||
| @Override | ||
| public void commandSucceeded(CommandSucceededEvent event) { | ||
| Span span = cache.remove(event.getRequestId()); | ||
| if (span != null) { | ||
| span.finish(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void commandFailed(CommandFailedEvent event) { | ||
| Span span = cache.remove(event.getRequestId()); | ||
| if (span != null) { | ||
| onError(span, event.getThrowable()); | ||
| span.finish(); | ||
| } | ||
| } | ||
|
|
||
| private Span buildSpan(CommandStartedEvent event) { | ||
| Tracer.SpanBuilder spanBuilder = | ||
| tracer.buildSpan(MONGO_OPERATION).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT); | ||
|
|
||
| Span span = spanBuilder.startManual(); | ||
| try { | ||
| decorate(span, event); | ||
| } catch (final Throwable e) { | ||
| log.warn("Couldn't decorate the mongo query: " + e.getMessage(), e); | ||
| } | ||
|
|
||
| return span; | ||
| } | ||
|
|
||
| private static void onError(Span span, Throwable throwable) { | ||
| Tags.ERROR.set(span, Boolean.TRUE); | ||
| span.log(Collections.singletonMap("error.object", throwable)); | ||
| } | ||
|
|
||
| public static void decorate(Span span, CommandStartedEvent event) { | ||
| // scrub the Mongo command so that parameters are removed from the string | ||
| final BsonDocument scrubbed = scrub(event.getCommand()); | ||
| final String mongoCmd = scrubbed.toString(); | ||
|
|
||
| Tags.COMPONENT.set(span, COMPONENT_NAME); | ||
| Tags.DB_STATEMENT.set(span, mongoCmd); | ||
| Tags.DB_INSTANCE.set(span, event.getDatabaseName()); | ||
| // add specific resource name | ||
| span.setTag(DDTags.RESOURCE_NAME, mongoCmd); | ||
| span.setTag(Tags.DB_STATEMENT.getKey(), mongoCmd); | ||
| span.setTag(DDTags.SPAN_TYPE, "mongodb"); | ||
|
||
| } catch (final Throwable e) { | ||
| log.warn("Couldn't decorate the mongo query: " + e.getMessage(), e); | ||
| } | ||
| } | ||
| span.setTag(DDTags.SERVICE_NAME, "mongo"); | ||
|
|
||
| Tags.PEER_HOSTNAME.set(span, event.getConnectionDescription().getServerAddress().getHost()); | ||
|
|
||
| InetAddress inetAddress = | ||
| event.getConnectionDescription().getServerAddress().getSocketAddress().getAddress(); | ||
|
|
||
| private BsonDocument norm(final BsonDocument origin) { | ||
| final BsonDocument normalized = new BsonDocument(); | ||
| for (final Map.Entry<String, BsonValue> entry : origin.entrySet()) { | ||
| if (WHILDCARD_FIELDS.contains(entry.getKey())) { | ||
| normalized.put(entry.getKey(), entry.getValue()); | ||
| if (inetAddress instanceof Inet4Address) { | ||
| byte[] address = inetAddress.getAddress(); | ||
| Tags.PEER_HOST_IPV4.set(span, ByteBuffer.wrap(address).getInt()); | ||
| } else { | ||
| final BsonValue child = norm(entry.getValue()); | ||
| normalized.put(entry.getKey(), child); | ||
| Tags.PEER_HOST_IPV6.set(span, inetAddress.getHostAddress()); | ||
| } | ||
|
|
||
| Tags.PEER_PORT.set(span, event.getConnectionDescription().getServerAddress().getPort()); | ||
| Tags.DB_TYPE.set(span, "mongo"); | ||
| } | ||
| return normalized; | ||
| } | ||
|
|
||
| private BsonValue norm(final BsonArray origin) { | ||
| final BsonArray normalized = new BsonArray(); | ||
| for (final BsonValue value : origin) { | ||
| final BsonValue child = norm(value); | ||
| normalized.add(child); | ||
| private static BsonDocument scrub(final BsonDocument origin) { | ||
| final BsonDocument scrub = new BsonDocument(); | ||
| for (final Map.Entry<String, BsonValue> entry : origin.entrySet()) { | ||
| if (UNSCRUBBED_FIELDS.contains(entry.getKey()) && entry.getValue().isString()) { | ||
| scrub.put(entry.getKey(), entry.getValue()); | ||
| } else { | ||
| final BsonValue child = scrub(entry.getValue()); | ||
| scrub.put(entry.getKey(), child); | ||
| } | ||
| } | ||
| return scrub; | ||
| } | ||
| return normalized; | ||
| } | ||
|
|
||
| private BsonValue norm(final BsonValue origin) { | ||
| private static BsonValue scrub(final BsonArray origin) { | ||
| final BsonArray scrub = new BsonArray(); | ||
| for (final BsonValue value : origin) { | ||
| final BsonValue child = scrub(value); | ||
| scrub.add(child); | ||
| } | ||
| return scrub; | ||
| } | ||
|
|
||
| final BsonValue normalized; | ||
| if (origin.isDocument()) { | ||
| normalized = norm(origin.asDocument()); | ||
| } else if (origin.isArray()) { | ||
| normalized = norm(origin.asArray()); | ||
| } else { | ||
| normalized = HIDDEN_CAR; | ||
| private static BsonValue scrub(final BsonValue origin) { | ||
| final BsonValue scrubbed; | ||
| if (origin.isDocument()) { | ||
| scrubbed = scrub(origin.asDocument()); | ||
| } else if (origin.isArray()) { | ||
| scrubbed = scrub(origin.asArray()); | ||
| } else { | ||
| scrubbed = HIDDEN_CHAR; | ||
| } | ||
| return scrubbed; | ||
| } | ||
| return normalized; | ||
| } | ||
| } | ||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kind of scares me a little... unbounded cache that relies on the behavior below for removal. What do you think @realark?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with it because the monog CommandListener api guarantees the started event will generate a failed/success event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other words, the mongo java driver itself would have to be buggy for the map to leak. We're not relying on good user code.