Skip to content

Commit e888f76

Browse files
tooryxcopybara-github
authored andcommitted
Introduce an exposed UI detector for ComfyUI. Written by Doyensec.
PiperOrigin-RevId: 733715954 Change-Id: I8fbcc6e8057c1a8116b8846feefcd744a53024ec
1 parent 3f40d26 commit e888f76

File tree

6 files changed

+513
-0
lines changed

6 files changed

+513
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# ComfyUI Detector
2+
3+
## Description
4+
5+
This plugins serves to detect the presence of an exposed ComfyUI instance.
6+
7+
## Build jar file for this plugin
8+
9+
Using `gradlew`:
10+
11+
```shell
12+
./gradlew jar
13+
```
14+
15+
The Tsunami identifiable jar file is located at `build/libs` directory.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
plugins {
2+
id 'java-library'
3+
}
4+
5+
description = 'ComfyUI Exposed UI'
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+
java {
18+
sourceCompatibility = JavaVersion.VERSION_11
19+
targetCompatibility = JavaVersion.VERSION_11
20+
21+
jar.manifest {
22+
attributes('Implementation-Title': name,
23+
'Implementation-Version': version,
24+
'Built-By': System.getProperty('user.name'),
25+
'Built-JDK': System.getProperty('java.version'),
26+
'Source-Compatibility': sourceCompatibility,
27+
'Target-Compatibility': targetCompatibility)
28+
}
29+
30+
javadoc.options {
31+
encoding = 'UTF-8'
32+
use = true
33+
links 'https://docs.oracle.com/javase/8/docs/api/'
34+
}
35+
36+
// Log stacktrace to console when test fails.
37+
test {
38+
testLogging {
39+
exceptionFormat = 'full'
40+
showExceptions true
41+
showCauses true
42+
showStackTraces true
43+
}
44+
maxHeapSize = '1500m'
45+
}
46+
}
47+
48+
ext {
49+
tsunamiVersion = 'latest.release'
50+
junitVersion = '4.13.1'
51+
mockitoVersion = '2.28.2'
52+
truthVersion = '1.0.1'
53+
guiceVersion = '4.2.3'
54+
}
55+
56+
dependencies {
57+
implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}"
58+
implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}"
59+
implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}"
60+
implementation 'org.jsoup:jsoup:1.9.2'
61+
62+
testImplementation "junit:junit:${junitVersion}"
63+
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
64+
testImplementation "com.google.inject:guice:${guiceVersion}"
65+
testImplementation "com.google.truth:truth:${truthVersion}"
66+
testImplementation "com.google.inject.extensions:guice-testlib:${guiceVersion}"
67+
testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}"
68+
testImplementation "com.google.truth.extensions:truth-proto-extension:${truthVersion}"
69+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'comfyui-exposed-ui'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
17+
package com.google.tsunami.plugins.detectors.comfyui.exposed;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static com.google.common.collect.ImmutableList.toImmutableList;
21+
22+
import com.google.common.annotations.VisibleForTesting;
23+
import com.google.common.collect.ImmutableList;
24+
import com.google.common.flogger.GoogleLogger;
25+
import com.google.protobuf.util.Timestamps;
26+
import com.google.tsunami.common.data.NetworkServiceUtils;
27+
import com.google.tsunami.common.net.http.HttpClient;
28+
import com.google.tsunami.common.net.http.HttpHeaders;
29+
import com.google.tsunami.common.net.http.HttpRequest;
30+
import com.google.tsunami.common.net.http.HttpResponse;
31+
import com.google.tsunami.common.net.http.HttpStatus;
32+
import com.google.tsunami.common.time.UtcClock;
33+
import com.google.tsunami.plugin.PluginType;
34+
import com.google.tsunami.plugin.VulnDetector;
35+
import com.google.tsunami.plugin.annotations.PluginInfo;
36+
import com.google.tsunami.proto.DetectionReport;
37+
import com.google.tsunami.proto.DetectionReportList;
38+
import com.google.tsunami.proto.DetectionStatus;
39+
import com.google.tsunami.proto.NetworkService;
40+
import com.google.tsunami.proto.Severity;
41+
import com.google.tsunami.proto.TargetInfo;
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 java.util.regex.Pattern;
48+
import javax.inject.Inject;
49+
import org.jsoup.Jsoup;
50+
import org.jsoup.nodes.Document;
51+
52+
/** A Tsunami plugin that detects an exposed instance of ComfyUI. */
53+
@PluginInfo(
54+
type = PluginType.VULN_DETECTION,
55+
name = "ComfyUI_exposedUI",
56+
version = "0.1",
57+
description = "This plugin detects an exposed instance of ComfyUI.",
58+
author = "Savino Sisco ([email protected]), Leonardo Giovannini ([email protected])",
59+
bootstrapModule = ComfyUiExposedUiBootstrapModule.class)
60+
public final class ComfyUiExposedUi implements VulnDetector {
61+
@VisibleForTesting static final String VULNERABILITY_REPORT_PUBLISHER = "TSUNAMI_COMMUNITY";
62+
63+
@VisibleForTesting static final String VULNERABILITY_REPORT_TITLE = "ComfyUI Exposed UI";
64+
65+
static final String VULNERABILITY_REPORT_DESCRIPTION =
66+
"The scanner detected an exposed ComfyUI instance.";
67+
68+
@VisibleForTesting
69+
static final String VULNERABILITY_REPORT_RECOMMENDATION = "Segregate the ComfyUI instance.";
70+
71+
@VisibleForTesting static final String MANAGER_VERSION_ENDPOINT = "api/manager/version";
72+
73+
@VisibleForTesting static final String STATS_ENDPOINT = "api/system_stats";
74+
75+
@VisibleForTesting
76+
static final Pattern VERSION_PATTERN = Pattern.compile("^V\\d+\\.\\d+\\.\\d+$");
77+
78+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
79+
private final Clock utcClock;
80+
private final HttpClient httpClient;
81+
82+
@Inject
83+
ComfyUiExposedUi(@UtcClock Clock utcClock, HttpClient httpClient) {
84+
this.utcClock = checkNotNull(utcClock);
85+
this.httpClient = checkNotNull(httpClient);
86+
}
87+
88+
// This is the main entry point of VulnDetector.
89+
@Override
90+
public DetectionReportList detect(
91+
TargetInfo targetInfo, ImmutableList<NetworkService> matchedServices) {
92+
logger.atInfo().log("ComfyUI Exposed UI starts detecting.");
93+
94+
return DetectionReportList.newBuilder()
95+
.addAllDetectionReports(
96+
matchedServices.stream()
97+
.filter(NetworkServiceUtils::isWebService)
98+
.filter(this::isComfyUi)
99+
.map(networkService -> buildDetectionReport(targetInfo, networkService))
100+
.collect(toImmutableList()))
101+
.build();
102+
}
103+
104+
/*
105+
* Fingerprint phase for ComfyUI.
106+
* This detects the service and the version
107+
*/
108+
private boolean isComfyUi(NetworkService networkService) {
109+
String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService);
110+
try {
111+
// Check web page title first
112+
String targetUri = rootUrl;
113+
HttpRequest req =
114+
HttpRequest.get(targetUri)
115+
.setHeaders(HttpHeaders.builder().addHeader("Accept", "application/json").build())
116+
.build();
117+
HttpResponse response;
118+
response = this.httpClient.send(req, networkService);
119+
Document doc = Jsoup.parse(response.bodyString().get());
120+
String title = doc.title();
121+
if (!title.contains("ComfyUI")) {
122+
return false;
123+
}
124+
125+
// Check the system_stats endpoint
126+
targetUri = rootUrl + STATS_ENDPOINT;
127+
req = HttpRequest.get(targetUri).withEmptyHeaders().build();
128+
response = this.httpClient.send(req, networkService);
129+
// Check if devices[0]["name"] is present
130+
try {
131+
if (response.bodyJson().isEmpty()
132+
|| response
133+
.bodyJson()
134+
.get()
135+
.getAsJsonObject()
136+
.get("devices")
137+
.getAsJsonArray()
138+
.get(0)
139+
.getAsJsonObject()
140+
.get("name")
141+
.getAsString()
142+
.isEmpty()) {
143+
return false;
144+
}
145+
} catch (NullPointerException | IllegalStateException | IndexOutOfBoundsException e) {
146+
return false;
147+
}
148+
logger.atInfo().log("ComfyUI Detected. Attempting to find version numbers.");
149+
150+
// Check if the Comfy version is available (not present on older versions)
151+
try {
152+
String comfyUiVersion =
153+
response
154+
.bodyJson()
155+
.get()
156+
.getAsJsonObject()
157+
.get("system")
158+
.getAsJsonObject()
159+
.get("comfyui_version")
160+
.getAsString();
161+
if (!comfyUiVersion.isEmpty()) {
162+
logger.atInfo().log("ComfyUI version: %s", comfyUiVersion);
163+
}
164+
} catch (NullPointerException | IllegalStateException | IndexOutOfBoundsException e) {
165+
// Do nothing, it's ok if the version is not there
166+
}
167+
168+
// Checking if ComfyUI Manager is available (not present on older versions)
169+
targetUri = rootUrl + MANAGER_VERSION_ENDPOINT;
170+
req = HttpRequest.get(targetUri).withEmptyHeaders().build();
171+
response = this.httpClient.send(req, networkService);
172+
if (response.status() == HttpStatus.OK
173+
&& VERSION_PATTERN.matcher(response.bodyString().orElse("")).find()) {
174+
logger.atInfo().log("ComfyUI Manager version: %s", response.bodyString().get());
175+
}
176+
} catch (IOException e) {
177+
return false;
178+
}
179+
return true;
180+
}
181+
182+
private DetectionReport buildDetectionReport(
183+
TargetInfo targetInfo, NetworkService vulnerableNetworkService) {
184+
185+
return DetectionReport.newBuilder()
186+
.setTargetInfo(targetInfo)
187+
.setNetworkService(vulnerableNetworkService)
188+
.setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli()))
189+
.setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
190+
.setVulnerability(
191+
Vulnerability.newBuilder()
192+
.setMainId(
193+
VulnerabilityId.newBuilder().setPublisher(VULNERABILITY_REPORT_PUBLISHER))
194+
.setSeverity(Severity.CRITICAL)
195+
.setTitle(VULNERABILITY_REPORT_TITLE)
196+
.setDescription(VULNERABILITY_REPORT_DESCRIPTION)
197+
.setRecommendation(VULNERABILITY_REPORT_RECOMMENDATION))
198+
.build();
199+
}
200+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
17+
package com.google.tsunami.plugins.detectors.comfyui.exposed;
18+
19+
import com.google.tsunami.plugin.PluginBootstrapModule;
20+
21+
/** A Guice module that bootstraps the {@link ComfyUiExposedUi}. */
22+
public final class ComfyUiExposedUiBootstrapModule extends PluginBootstrapModule {
23+
24+
@Override
25+
protected void configurePlugin() {
26+
registerPlugin(ComfyUiExposedUi.class);
27+
}
28+
}

0 commit comments

Comments
 (0)