diff --git a/community/README.md b/community/README.md index e651c1cea..4bd11e3bc 100644 --- a/community/README.md +++ b/community/README.md @@ -47,7 +47,7 @@ This directory contains plugins contributed by community members. * [Exposed Argo Workflows Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/argoworkflows_exposed_ui) * [Uptrain Exposed API VulnDetector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/uptrain_exposed_api) * [CVE-2025-0655 D-Tale Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/dtale_cve_2025_0655) -* [Flowise Exposed UI Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/flowise_exposed_ui) +* [Flowise Exposed UI Detector, now updated to Templated format](https://github.com/google/tsunami-security-scanner-plugins/tree/master/templated/templateddetector/plugins/exposedui/Flowise_ExposedUI.textproto) #### XML External Entity (XXE) Injection diff --git a/community/detectors/flowise_exposed_ui/README.md b/community/detectors/flowise_exposed_ui/README.md deleted file mode 100644 index d0a1a2c96..000000000 --- a/community/detectors/flowise_exposed_ui/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Flowise UI Exposed Detector - -This Tsunami plugin detects exposed Flowise UI instances. Flowise is a drag & -drop UI tool for building LLM applications. When exposed without proper -authentication, it could lead to unauthorized access and potential security -risks. - -## Description - -The detector performs the following checks: - Attempts to access the Flowise UI -endpoint - Verifies if the API interface is accessible without authentication - -## Build jar file for this plugin - -Using `gradlew`: - -```shell -./gradlew jar -``` - -Tsunami identifiable jar file is located at `build/libs` directory. diff --git a/community/detectors/flowise_exposed_ui/build.gradle b/community/detectors/flowise_exposed_ui/build.gradle deleted file mode 100644 index ef3924f7b..000000000 --- a/community/detectors/flowise_exposed_ui/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - id 'java-library' -} - -description = 'Tsunami Flowise UI exposed VulnDetector plugin.' -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.google.truth:truth:1.4.4" - testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" -} - diff --git a/community/detectors/flowise_exposed_ui/settings.gradle b/community/detectors/flowise_exposed_ui/settings.gradle deleted file mode 100644 index 70c5a0ad0..000000000 --- a/community/detectors/flowise_exposed_ui/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'flowise_exposed_ui' - -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") - } -} \ No newline at end of file diff --git a/community/detectors/flowise_exposed_ui/src/main/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetector.java b/community/detectors/flowise_exposed_ui/src/main/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetector.java deleted file mode 100644 index 45bf63cfc..000000000 --- a/community/detectors/flowise_exposed_ui/src/main/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetector.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2025 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.exposedui.flowise; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableList.toImmutableList; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.flogger.GoogleLogger; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.data.NetworkServiceUtils; -import com.google.tsunami.common.net.http.HttpClient; -import com.google.tsunami.common.net.http.HttpHeaders; -import com.google.tsunami.common.net.http.HttpRequest; -import com.google.tsunami.common.net.http.HttpResponse; -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.Vulnerability; -import com.google.tsunami.proto.VulnerabilityId; -import java.io.IOException; -import java.time.Clock; -import java.time.Instant; -import javax.inject.Inject; - -/** A {@link VulnDetector} that detects an exposed Flowise UI installation. */ -@PluginInfo( - type = PluginType.VULN_DETECTION, - name = "FlowiseExposedUiDetector", - version = "0.1", - description = - "This detector checks whether a Flowise UI installation is exposed without proper" - + " authentication.", - author = "yuradoc (yuradoc.research@gmail.com)", - bootstrapModule = FlowiseExposedUiDetectorBootstrapModule.class) -public final class FlowiseExposedUiDetector implements VulnDetector { - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - private final Clock utcClock; - private final HttpClient httpClient; - - @VisibleForTesting - static final String RECOMMENDATION = - "Please disable public access to your Flowise instance. You can enable authentication" - + " for your instance by following the instructions here:" - + " https://docs.flowiseai.com/configuration/authorization/app-level"; - - @Inject - FlowiseExposedUiDetector(@UtcClock Clock utcClock, HttpClient httpClient) { - this.utcClock = checkNotNull(utcClock); - this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(false).build(); - } - - @Override - public ImmutableList getAdvisories() { - return ImmutableList.of( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher("TSUNAMI_COMMUNITY") - .setValue("FLOWISE_UI_EXPOSED")) - .setSeverity(Severity.HIGH) - .setTitle("Flowise UI Exposed") - .setDescription("Flowise UI instance is exposed without proper authentication.") - .setRecommendation(RECOMMENDATION) - .build()); - } - - @Override - public DetectionReportList detect( - TargetInfo targetInfo, ImmutableList matchedServices) { - logger.atInfo().log("FlowiseExposedUiDetector starts detecting."); - - return DetectionReportList.newBuilder() - .addAllDetectionReports( - matchedServices.stream() - .filter(NetworkServiceUtils::isWebService) - .filter(this::isServiceVulnerable) - .map(networkService -> buildDetectionReport(targetInfo, networkService)) - .collect(toImmutableList())) - .build(); - } - - private boolean isServiceVulnerable(NetworkService networkService) { - String targetUri = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); - String targetApiUri = targetUri + "api/v1/apikey"; - - HttpResponse response; - try { - // plain GET request to check Flowise UI availability. - response = - httpClient.send(HttpRequest.get(targetUri).withEmptyHeaders().build(), networkService); - if (!(response.bodyString().isPresent() && response.bodyString().get().contains("Flowise"))) { - return false; - } - - // Main request that performs vulnerability check. - response = - httpClient.send( - HttpRequest.get(targetApiUri) - .setHeaders(HttpHeaders.builder().addHeader("x-request-from", "internal").build()) - .build(), - networkService); - return response.status().code() != 401; - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query Flowise."); - return false; - } - } - - private DetectionReport buildDetectionReport( - TargetInfo targetInfo, NetworkService vulnerableNetworkService) { - return DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(vulnerableNetworkService) - .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability( - getAdvisories().get(0).toBuilder() - .addAdditionalDetails( - AdditionalDetail.newBuilder() - .setTextData( - TextData.newBuilder() - .setText( - String.format( - "The Flowise UI instance at %s is exposed without proper" - + " authentication.", - NetworkServiceUtils.buildWebApplicationRootUrl( - vulnerableNetworkService)))))) - .build(); - } -} diff --git a/community/detectors/flowise_exposed_ui/src/main/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetectorBootstrapModule.java b/community/detectors/flowise_exposed_ui/src/main/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetectorBootstrapModule.java deleted file mode 100644 index f60f4717b..000000000 --- a/community/detectors/flowise_exposed_ui/src/main/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetectorBootstrapModule.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 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.exposedui.flowise; - -import com.google.tsunami.plugin.PluginBootstrapModule; - -/** A {@link PluginBootstrapModule} for {@link FlowiseExposedUiDetector}. */ -public final class FlowiseExposedUiDetectorBootstrapModule extends PluginBootstrapModule { - @Override - protected void configurePlugin() { - registerPlugin(FlowiseExposedUiDetector.class); - } -} diff --git a/community/detectors/flowise_exposed_ui/src/test/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetectorTest.java b/community/detectors/flowise_exposed_ui/src/test/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetectorTest.java deleted file mode 100644 index 47e5452b8..000000000 --- a/community/detectors/flowise_exposed_ui/src/test/java/com/google/tsunami/plugins/detectors/exposedui/flowise/FlowiseExposedUiDetectorTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.google.tsunami.plugins.detectors.exposedui.flowise; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.inject.Guice; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.data.NetworkEndpointUtils; -import com.google.tsunami.common.data.NetworkServiceUtils; -import com.google.tsunami.common.net.http.HttpClientModule; -import com.google.tsunami.common.net.http.HttpStatus; -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.IOException; -import java.time.Instant; -import javax.inject.Inject; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class FlowiseExposedUiDetectorTest { - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2025-03-22T00:00:00.00Z")); - - static final String FLOWISE_PRESENT_STR = "Flowise - Low-code LLM apps builder"; - - private MockWebServer mockWebServer; - - @Inject private FlowiseExposedUiDetector detector; - - private NetworkService service; - private TargetInfo targetInfo; - - @Before - public void setUp() { - mockWebServer = new MockWebServer(); - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - new FlowiseExposedUiDetectorBootstrapModule()) - .injectMembers(this); - - targetInfo = - TargetInfo.newBuilder() - .addNetworkEndpoints(NetworkEndpointUtils.forHostname(mockWebServer.getHostName())) - .build(); - service = - NetworkService.newBuilder() - .setNetworkEndpoint( - NetworkEndpointUtils.forHostnameAndPort( - mockWebServer.getHostName(), mockWebServer.getPort())) - .setTransportProtocol(TransportProtocol.TCP) - .setServiceName("http") - .build(); - } - - @After - public void tearDown() throws IOException { - mockWebServer.shutdown(); - } - - @Test - public void detect_whenFlowiseUiExposed_returnsVulnerability() throws IOException { - mockWebServer.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(FLOWISE_PRESENT_STR)); - mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("[]")); - - DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()) - .containsExactly( - DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(service) - .setDetectionTimestamp( - Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability( - detector.getAdvisories().get(0).toBuilder() - .addAdditionalDetails( - AdditionalDetail.newBuilder() - .setTextData( - TextData.newBuilder() - .setText( - String.format( - "The Flowise UI instance at %s is exposed without" - + " proper authentication.", - NetworkServiceUtils.buildWebApplicationRootUrl( - service)))))) - .build()); - } - - @Test - public void detect_whenFlowiseUiNotPresent_returnsNoVulnerability() { - mockWebServer.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("Some other content")); - - DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - } - - @Test - public void detect_whenFlowiseUiPresent_butApiProtected_returnsNoVulnerability() { - mockWebServer.enqueue( - new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(FLOWISE_PRESENT_STR)); - mockWebServer.enqueue( - new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code()).setBody("")); - - DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - } -} diff --git a/templated/templateddetector/plugins/exposedui/Flowise_ExposedUI.textproto b/templated/templateddetector/plugins/exposedui/Flowise_ExposedUI.textproto new file mode 100644 index 000000000..b233147cc --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Flowise_ExposedUI.textproto @@ -0,0 +1,80 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "Flowise_ExposedUI" + author: + "Robert Dick (robert@doyensec.com) for the Templated version, " + "yuradoc (yuradoc.research@gmail.com)" + version: "2.0" +} + +finding: { + main_id: { + publisher: "TSUNAMI_COMMUNITY" + value: "FLOWISE_UI_EXPOSED" + } + title: "Flowise Exposed UI" + description: "Flowise UI instance is exposed without proper authentication." + recommendation: + "Please disable public access to your Flowise instance. You can enable authentication" + " for your instance by following the instructions here:" + " https://docs.flowiseai.com/configuration/authorization/app-level" + severity: HIGH +} + +########### +# ACTIONS # +########### + +actions: { + name: "fingerprint_flowise" + http_request: { + method: GET + uri: "/" + response: { + http_status: 200 + expect_all: { + conditions: { body {} contains: 'Flowise' } + } + } + } +} + +# original Java plugin only checked if response here wasn't 200. + +actions: { + name: "check_apikey" + http_request: { + method: GET + uri: "/api/v1/apikey" + headers: [ + { name: "x-request-from" value: "internal" } + ] + response: { + http_status: 200 + expect_all: { + conditions: [ + { body {} contains: '"apiKey":' }, + { body {} contains: '"apiSecret":' } + ] + } + } + } +} + +############# +# WORKFLOWS # +############# + +workflows: { + actions: [ + "fingerprint_flowise", + "check_apikey" + ] +} \ No newline at end of file diff --git a/templated/templateddetector/plugins/exposedui/Flowise_ExposedUI_test.textproto b/templated/templateddetector/plugins/exposedui/Flowise_ExposedUI_test.textproto new file mode 100644 index 000000000..8b25e36df --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Flowise_ExposedUI_test.textproto @@ -0,0 +1,67 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "Flowise_ExposedUI" +} + +tests: { + name: "whenVulnerable_returnsVuln" + expect_vulnerability: true + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: '... Flowise ...' + }, + { + uri: "/api/v1/apikey" + status: 200 + body_content: + '[{"keyName":"DefaultKey","apiKey":"_not_a_real_secret__not_a_real_secret_"' + ',"apiSecret":"_not_a_real_secret__not_a_real_secret__not_a_real_secret_' + '_not_a_real_secret__not_a_real_secret__not_a_real_secret__not_a_real_secret_"' + ',"createdAt":"08-May-26","id":"d4085acd579159393200089ce83dcf7f","chatFlows":[]}]' + } + ] + } +} + +tests: { + name: "whenAuthReqd_returnsNotVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: '... Flowise ...' + }, + { + uri: "/api/v1/apikey" + status: 401 + body_content: + 'Unauthorized' + } + ] + } +} + + +tests: { + name: "whenRandomServer_returnsNotVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "... Hello world ..." + } + ] + } +}