Skip to content

Commit 402fe0a

Browse files
Merge pull request #620 from YuriyPobezhymov:flowise_exposed_ui
PiperOrigin-RevId: 791256807 Change-Id: I1f44a850bbe4e04860be128eac8a7062f84a21ff
2 parents c109565 + 9883d7e commit 402fe0a

7 files changed

Lines changed: 370 additions & 0 deletions

File tree

community/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ This directory contains plugins contributed by community members.
4747
* [Exposed Argo Workflows Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/argoworkflows_exposed_ui)
4848
* [Uptrain Exposed API VulnDetector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/uptrain_exposed_api)
4949
* [CVE-2025-0655 D-Tale Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/dtale_cve_2025_0655)
50+
* [Flowise Exposed UI Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/flowise_exposed_ui)
5051

5152
#### XML External Entity (XXE) Injection
5253

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Flowise UI Exposed Detector
2+
3+
This Tsunami plugin detects exposed Flowise UI instances. Flowise is a drag &
4+
drop UI tool for building LLM applications. When exposed without proper
5+
authentication, it could lead to unauthorized access and potential security
6+
risks.
7+
8+
## Description
9+
10+
The detector performs the following checks: - Attempts to access the Flowise UI
11+
endpoint - Verifies if the API interface is accessible without authentication
12+
13+
## Build jar file for this plugin
14+
15+
Using `gradlew`:
16+
17+
```shell
18+
./gradlew jar
19+
```
20+
21+
Tsunami identifiable jar file is located at `build/libs` directory.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
plugins {
2+
id 'java-library'
3+
}
4+
5+
description = 'Tsunami Flowise UI exposed VulnDetector plugin.'
6+
group = 'com.google.tsunami'
7+
version = '0.0.1-SNAPSHOT'
8+
9+
repositories {
10+
maven { // The google mirror is less flaky than mavenCentral()
11+
url 'https://maven-central.storage-download.googleapis.com/repos/central/data/'
12+
}
13+
mavenCentral()
14+
mavenLocal()
15+
}
16+
17+
dependencies {
18+
implementation("com.google.tsunami:tsunami-common") {
19+
version { branch = "stable" }
20+
}
21+
implementation("com.google.tsunami:tsunami-plugin") {
22+
version { branch = "stable" }
23+
}
24+
implementation("com.google.tsunami:tsunami-proto") {
25+
version { branch = "stable" }
26+
}
27+
28+
testImplementation "junit:junit:4.13.2"
29+
testImplementation "com.google.truth:truth:1.4.4"
30+
testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0"
31+
}
32+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
rootProject.name = 'flowise_exposed_ui'
2+
sourceControl {
3+
gitRepository("https://github.com/google/tsunami-security-scanner.git") {
4+
producesModule("com.google.tsunami:tsunami-common")
5+
producesModule("com.google.tsunami:tsunami-plugin")
6+
producesModule("com.google.tsunami:tsunami-proto")
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.tsunami.plugins.detectors.exposedui.flowise;
17+
18+
import static com.google.common.base.Preconditions.checkNotNull;
19+
import static com.google.common.collect.ImmutableList.toImmutableList;
20+
21+
import com.google.common.annotations.VisibleForTesting;
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.common.flogger.GoogleLogger;
24+
import com.google.protobuf.util.Timestamps;
25+
import com.google.tsunami.common.data.NetworkServiceUtils;
26+
import com.google.tsunami.common.net.http.HttpClient;
27+
import com.google.tsunami.common.net.http.HttpHeaders;
28+
import com.google.tsunami.common.net.http.HttpRequest;
29+
import com.google.tsunami.common.net.http.HttpResponse;
30+
import com.google.tsunami.common.time.UtcClock;
31+
import com.google.tsunami.plugin.PluginType;
32+
import com.google.tsunami.plugin.VulnDetector;
33+
import com.google.tsunami.plugin.annotations.PluginInfo;
34+
import com.google.tsunami.proto.AdditionalDetail;
35+
import com.google.tsunami.proto.DetectionReport;
36+
import com.google.tsunami.proto.DetectionReportList;
37+
import com.google.tsunami.proto.DetectionStatus;
38+
import com.google.tsunami.proto.NetworkService;
39+
import com.google.tsunami.proto.Severity;
40+
import com.google.tsunami.proto.TargetInfo;
41+
import com.google.tsunami.proto.TextData;
42+
import com.google.tsunami.proto.Vulnerability;
43+
import com.google.tsunami.proto.VulnerabilityId;
44+
import java.io.IOException;
45+
import java.time.Clock;
46+
import java.time.Instant;
47+
import javax.inject.Inject;
48+
49+
/** A {@link VulnDetector} that detects an exposed Flowise UI installation. */
50+
@PluginInfo(
51+
type = PluginType.VULN_DETECTION,
52+
name = "FlowiseExposedUiDetector",
53+
version = "0.1",
54+
description =
55+
"This detector checks whether a Flowise UI installation is exposed without proper"
56+
+ " authentication.",
57+
author = "yuradoc (yuradoc.research@gmail.com)",
58+
bootstrapModule = FlowiseExposedUiDetectorBootstrapModule.class)
59+
public final class FlowiseExposedUiDetector implements VulnDetector {
60+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
61+
62+
private final Clock utcClock;
63+
private final HttpClient httpClient;
64+
65+
@VisibleForTesting
66+
static final String RECOMMENDATION =
67+
"Please disable public access to your Flowise instance. You can enable authentication"
68+
+ " for your instance by following the instructions here:"
69+
+ " https://docs.flowiseai.com/configuration/authorization/app-level";
70+
71+
@Inject
72+
FlowiseExposedUiDetector(@UtcClock Clock utcClock, HttpClient httpClient) {
73+
this.utcClock = checkNotNull(utcClock);
74+
this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(false).build();
75+
}
76+
77+
@Override
78+
public ImmutableList<Vulnerability> getAdvisories() {
79+
return ImmutableList.of(
80+
Vulnerability.newBuilder()
81+
.setMainId(
82+
VulnerabilityId.newBuilder()
83+
.setPublisher("TSUNAMI_COMMUNITY")
84+
.setValue("FLOWISE_UI_EXPOSED"))
85+
.setSeverity(Severity.HIGH)
86+
.setTitle("Flowise UI Exposed")
87+
.setDescription("Flowise UI instance is exposed without proper authentication.")
88+
.setRecommendation(RECOMMENDATION)
89+
.build());
90+
}
91+
92+
@Override
93+
public DetectionReportList detect(
94+
TargetInfo targetInfo, ImmutableList<NetworkService> matchedServices) {
95+
logger.atInfo().log("FlowiseExposedUiDetector starts detecting.");
96+
97+
return DetectionReportList.newBuilder()
98+
.addAllDetectionReports(
99+
matchedServices.stream()
100+
.filter(NetworkServiceUtils::isWebService)
101+
.filter(this::isServiceVulnerable)
102+
.map(networkService -> buildDetectionReport(targetInfo, networkService))
103+
.collect(toImmutableList()))
104+
.build();
105+
}
106+
107+
private boolean isServiceVulnerable(NetworkService networkService) {
108+
String targetUri = NetworkServiceUtils.buildWebApplicationRootUrl(networkService);
109+
String targetApiUri = targetUri + "api/v1/apikey";
110+
111+
HttpResponse response;
112+
try {
113+
// plain GET request to check Flowise UI availability.
114+
response =
115+
httpClient.send(HttpRequest.get(targetUri).withEmptyHeaders().build(), networkService);
116+
if (!(response.bodyString().isPresent() && response.bodyString().get().contains("Flowise"))) {
117+
return false;
118+
}
119+
120+
// Main request that performs vulnerability check.
121+
response =
122+
httpClient.send(
123+
HttpRequest.get(targetApiUri)
124+
.setHeaders(HttpHeaders.builder().addHeader("x-request-from", "internal").build())
125+
.build(),
126+
networkService);
127+
return response.status().code() != 401;
128+
} catch (IOException e) {
129+
logger.atWarning().withCause(e).log("Unable to query Flowise.");
130+
return false;
131+
}
132+
}
133+
134+
private DetectionReport buildDetectionReport(
135+
TargetInfo targetInfo, NetworkService vulnerableNetworkService) {
136+
return DetectionReport.newBuilder()
137+
.setTargetInfo(targetInfo)
138+
.setNetworkService(vulnerableNetworkService)
139+
.setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli()))
140+
.setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
141+
.setVulnerability(
142+
getAdvisories().get(0).toBuilder()
143+
.addAdditionalDetails(
144+
AdditionalDetail.newBuilder()
145+
.setTextData(
146+
TextData.newBuilder()
147+
.setText(
148+
String.format(
149+
"The Flowise UI instance at %s is exposed without proper"
150+
+ " authentication.",
151+
NetworkServiceUtils.buildWebApplicationRootUrl(
152+
vulnerableNetworkService))))))
153+
.build();
154+
}
155+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.tsunami.plugins.detectors.exposedui.flowise;
17+
18+
import com.google.tsunami.plugin.PluginBootstrapModule;
19+
20+
/** A {@link PluginBootstrapModule} for {@link FlowiseExposedUiDetector}. */
21+
public final class FlowiseExposedUiDetectorBootstrapModule extends PluginBootstrapModule {
22+
@Override
23+
protected void configurePlugin() {
24+
registerPlugin(FlowiseExposedUiDetector.class);
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.google.tsunami.plugins.detectors.exposedui.flowise;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
5+
import com.google.common.collect.ImmutableList;
6+
import com.google.inject.Guice;
7+
import com.google.protobuf.util.Timestamps;
8+
import com.google.tsunami.common.data.NetworkEndpointUtils;
9+
import com.google.tsunami.common.data.NetworkServiceUtils;
10+
import com.google.tsunami.common.net.http.HttpClientModule;
11+
import com.google.tsunami.common.net.http.HttpStatus;
12+
import com.google.tsunami.common.time.testing.FakeUtcClock;
13+
import com.google.tsunami.common.time.testing.FakeUtcClockModule;
14+
import com.google.tsunami.proto.AdditionalDetail;
15+
import com.google.tsunami.proto.DetectionReport;
16+
import com.google.tsunami.proto.DetectionReportList;
17+
import com.google.tsunami.proto.DetectionStatus;
18+
import com.google.tsunami.proto.NetworkService;
19+
import com.google.tsunami.proto.TargetInfo;
20+
import com.google.tsunami.proto.TextData;
21+
import com.google.tsunami.proto.TransportProtocol;
22+
import java.io.IOException;
23+
import java.time.Instant;
24+
import javax.inject.Inject;
25+
import okhttp3.mockwebserver.MockResponse;
26+
import okhttp3.mockwebserver.MockWebServer;
27+
import org.junit.After;
28+
import org.junit.Before;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
import org.junit.runners.JUnit4;
32+
33+
@RunWith(JUnit4.class)
34+
public final class FlowiseExposedUiDetectorTest {
35+
private final FakeUtcClock fakeUtcClock =
36+
FakeUtcClock.create().setNow(Instant.parse("2025-03-22T00:00:00.00Z"));
37+
38+
static final String FLOWISE_PRESENT_STR = "Flowise - Low-code LLM apps builder";
39+
40+
private MockWebServer mockWebServer;
41+
42+
@Inject private FlowiseExposedUiDetector detector;
43+
44+
private NetworkService service;
45+
private TargetInfo targetInfo;
46+
47+
@Before
48+
public void setUp() {
49+
mockWebServer = new MockWebServer();
50+
Guice.createInjector(
51+
new FakeUtcClockModule(fakeUtcClock),
52+
new HttpClientModule.Builder().build(),
53+
new FlowiseExposedUiDetectorBootstrapModule())
54+
.injectMembers(this);
55+
56+
targetInfo =
57+
TargetInfo.newBuilder()
58+
.addNetworkEndpoints(NetworkEndpointUtils.forHostname(mockWebServer.getHostName()))
59+
.build();
60+
service =
61+
NetworkService.newBuilder()
62+
.setNetworkEndpoint(
63+
NetworkEndpointUtils.forHostnameAndPort(
64+
mockWebServer.getHostName(), mockWebServer.getPort()))
65+
.setTransportProtocol(TransportProtocol.TCP)
66+
.setServiceName("http")
67+
.build();
68+
}
69+
70+
@After
71+
public void tearDown() throws IOException {
72+
mockWebServer.shutdown();
73+
}
74+
75+
@Test
76+
public void detect_whenFlowiseUiExposed_returnsVulnerability() throws IOException {
77+
mockWebServer.enqueue(
78+
new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(FLOWISE_PRESENT_STR));
79+
mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("[]"));
80+
81+
DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service));
82+
83+
assertThat(detectionReports.getDetectionReportsList())
84+
.containsExactly(
85+
DetectionReport.newBuilder()
86+
.setTargetInfo(targetInfo)
87+
.setNetworkService(service)
88+
.setDetectionTimestamp(
89+
Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli()))
90+
.setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
91+
.setVulnerability(
92+
detector.getAdvisories().get(0).toBuilder()
93+
.addAdditionalDetails(
94+
AdditionalDetail.newBuilder()
95+
.setTextData(
96+
TextData.newBuilder()
97+
.setText(
98+
String.format(
99+
"The Flowise UI instance at %s is exposed without"
100+
+ " proper authentication.",
101+
NetworkServiceUtils.buildWebApplicationRootUrl(
102+
service))))))
103+
.build());
104+
}
105+
106+
@Test
107+
public void detect_whenFlowiseUiNotPresent_returnsNoVulnerability() {
108+
mockWebServer.enqueue(
109+
new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody("Some other content"));
110+
111+
DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service));
112+
113+
assertThat(detectionReports.getDetectionReportsList()).isEmpty();
114+
}
115+
116+
@Test
117+
public void detect_whenFlowiseUiPresent_butApiProtected_returnsNoVulnerability() {
118+
mockWebServer.enqueue(
119+
new MockResponse().setResponseCode(HttpStatus.OK.code()).setBody(FLOWISE_PRESENT_STR));
120+
mockWebServer.enqueue(
121+
new MockResponse().setResponseCode(HttpStatus.UNAUTHORIZED.code()).setBody(""));
122+
123+
DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service));
124+
125+
assertThat(detectionReports.getDetectionReportsList()).isEmpty();
126+
}
127+
}

0 commit comments

Comments
 (0)