Vulnerabilities in pkg:npm/next-tinacms-dos
PACKAGECVE-2026-59992 is a MODERATE (CVSS 5.4) broken access control: arbitrary bucket-key vulnerability (CWE-639: Authorization Bypass Through User-Controlled Key) affecting Tina libraries in the npm ecosystem. The issue involves broken access control: arbitrary bucket-key write/delete in `next-tinacms-s3` (and sibling production media adapters). Remediation is available by upgrading affected packages to version 23.0.4, 14.0.4, 26.0.4.
pkg:npm/next-tinacms-dos
Vulnerability advisories affecting package pkg:npm/next-tinacms-dos. Select an advisory below to view technical details or fixes.
| Advisory ID | CVE / NVD | Severity | Summary |
|---|---|---|---|
| GHSA-8mq9-5fw2-5rm4 | CVE-2026-59992 | MODERATE | Tina: Broken Access Control: arbitrary bucket-key write/delete in `next-tinacms-s3` (and sibling production media adapters) |
CVE-2026-59992 at a Glance
Security Impact
CVSS: 5.4
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L).
How to Fix CVE-2026-59992
To remediate this vulnerability, upgrade all affected Tina dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install next-tinacms-s3@23.0.4
npm install next-tinacms-dos@23.0.4
npm install next-tinacms-azure@14.0.4
npm install next-tinacms-cloudinary@26.0.4
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls next-tinacms-s3
How to Check if You Are Affected
To determine whether your Node.js application is affected by CVE-2026-59992, inspect your project dependencies using npm:
npm ls next-tinacms-s3
Analyze the terminal output to locate any version of Tina in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 23.0.4), your application is affected.
Note: The npm ls next-tinacms-s3 command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-639: Authorization Bypass Through User-Controlled Key, where the system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. In Tina, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-59992 Affected Tina Versions
4 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
next-tinacms-s3
|
npm |
< 23.0.4
|
23.0.4
|
|
next-tinacms-dos
|
npm |
< 23.0.4
|
23.0.4
|
|
next-tinacms-azure
|
npm |
< 14.0.4
|
14.0.4
|
|
next-tinacms-cloudinary
|
npm |
< 26.0.4
|
26.0.4
|
References
Frequently Asked Questions
What is CVE-2026-59992?
Which Tina versions are affected?
What version fixes CVE-2026-59992?
Is next-tinacms-s3 affected?
Is next-tinacms-dos affected?
What is the CVSS score of CVE-2026-59992?
What CWE is associated with CVE-2026-59992?
How can I check npm dependencies for CVE-2026-59992?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-8mq9-5fw2-5rm4",
"modified" : "2026-08-21T18:31:05Z",
"published" : "2026-08-19T21:56:30Z",
"aliases" : [ "CVE-2026-59992" ],
"summary" : "Tina: Broken Access Control: arbitrary bucket-key write/delete in `next-tinacms-s3` (and sibling production media adapters)",
"details" : "## Summary\n\nThe production media handler shipped by `next-tinacms-s3` (`createMediaHandler` in `packages/next-tinacms-s3/src/handlers.ts`) accepts an attacker-chosen `?key=` query parameter and returns an AWS-signed `PutObject` URL whose `Key` is that value, with no check that the key falls under the operator's configured `mediaRoot`. The same handler's `DELETE` branch reads `objectKey = (req.query.media as string[])[1]` and dispatches a `DeleteObjectCommand` for that exact key, again unbounded by `mediaRoot`. Any caller that passes the operator-supplied `authorized()` predicate — i.e. any logged-in CMS editor in a typical TinaCloud / self-hosted deployment — therefore has write and delete authority over the *entire* S3 bucket the IAM key can reach, even though the package documents `mediaRoot` as the place where editors are scoped. The same shape is present in `next-tinacms-dos`, `next-tinacms-azure`, and `next-tinacms-cloudinary`, so a single design mistake spans every first-party production media backend.\n\n* Project: TinaCMS — first-party production media adapters (consumed by self-hosted Next.js sites and TinaCloud-backed deployments).\n* Source reviewed: `tinacms/tinacms` @ `main` (`b56dad4`).\n* Deployed artefact validated: `next-tinacms-s3@21.0.3` handler logic, exercised against `@aws-sdk/client-s3@3.665.x` via `aws-sdk-client-mock@4.1.0` (the AWS SDK signs the URL identically whether the bucket is real or mocked).\n* Affected file(s):\n * `packages/next-tinacms-s3/src/handlers.ts:67-90` — `GET ?key=` returns presigned `PutObjectCommand` URL with attacker-chosen `Key`.\n * `packages/next-tinacms-s3/src/handlers.ts:199-223` — `DELETE` reads `[, objectKey] = media` and issues `DeleteObjectCommand` against attacker-chosen `Key`.\n * `packages/next-tinacms-dos/src/handlers.ts:79-152` and `:249-278` — same write/delete pattern, plus a server-side upload that builds the key with `path.join(mediaRoot, prefix + filename)` over attacker-controlled `directory` and `filename`.\n * `packages/next-tinacms-azure/src/handlers.ts:44-95` — `uploadMedia` writes `path.join(directory, filename)` with both fields attacker-controlled (no `mediaRoot` configured at all); `deleteAsset` deletes any blob in the container.\n * `packages/next-tinacms-cloudinary/src/handlers.ts:193-204` — `cloudinary.uploader.destroy(public_id)` over attacker-chosen `public_id`.\n* CWE: CWE-639 — Authorization Bypass Through User-Controlled Key. Adjacent: CWE-284 (Improper Access Control), CWE-862 (Missing Authorization on the per-key authority check).\n* OWASP 2021: **A01:2021 — Broken Access Control** (the operator's intended `mediaRoot` boundary is enforced only on listing, not on writes or deletes). Secondary: A04:2021 — Insecure Design (every adapter independently re-implements the same broken pattern).\n\n## Vulnerable code\n\n`packages/next-tinacms-s3/src/handlers.ts:39-98`:\n\n```ts\nexport const createMediaHandler = (config: S3Config, options?: S3Options) => {\n const client = new S3Client(config.config);\n const bucket = config.bucket;\n let mediaRoot = config.mediaRoot || ''; // (1)\n if (mediaRoot) { /* normalise to \"media/\" form */ }\n\n return async (req: NextApiRequest, res: NextApiResponse) => {\n const isAuthorized = await config.authorized(req, res);\n if (!isAuthorized) {\n res.status(401).json({ message: 'sorry this user is unauthorized' });\n return;\n }\n switch (req.method) {\n case 'GET':\n if (req.query.key) {\n const expiresIn: number =\n (req.query.expiresIn && Number(req.query.expiresIn)) || 3600; // (2)\n const s3_key = req.query.key\n ? Array.isArray(req.query.key) ? req.query.key[0] : req.query.key\n : null;\n if (!s3_key) return res.status(400).json({ message: 'key is required' });\n if (await keyExists(client, bucket, s3_key)) {\n return res.status(400).json({ message: 'key already exists' }); // (3)\n }\n const signedUrl = await getUploadUrl(bucket, s3_key, expiresIn, client); // (4)\n return res.json({ signedUrl, src: cdnUrl + s3_key });\n }\n return listMedia(req, res, client, bucket, mediaRoot, cdnUrl); // (5)\n case 'DELETE':\n return deleteAsset(req, res, client, bucket); // (6)\n```\n\n`packages/next-tinacms-s3/src/handlers.ts:199-223`:\n\n```ts\nasync function deleteAsset(req, res, client, bucket) {\n const { media } = req.query;\n const [, objectKey] = media as string[]; // (7)\n const params: DeleteObjectCommandInput = { Bucket: bucket, Key: objectKey };\n const command = new DeleteObjectCommand(params);\n ...\n}\n```\n\nAt **(1)** the operator configures `mediaRoot` (e.g. `\"media/\"`), and the package's `README` documents this as the directory the IAM key is scoped to. At **(4)** the handler signs a `PutObjectCommand` with `Key: s3_key` taken verbatim from `req.query.key`. Nothing between (1) and (4) verifies that `s3_key` starts with `mediaRoot`, so any path the IAM key can reach is fair game. The only filter is the `keyExists` check at **(3)**, which prevents *overwrite* of an existing object but not creation of arbitrary new ones (and not overwrite of objects the IAM key cannot `HeadObject`). At **(2)** the validity window of the produced URL is also attacker-controlled, capped only by AWS SigV4's 7-day hard limit. At **(5)** the *list* path **does** prefix-join `mediaRoot` (`Prefix: mediaRoot ? path.join(mediaRoot, prefix) : prefix`), demonstrating that the boundary was understood to exist — it just isn't enforced on writes. At **(6)** + **(7)** delete extracts the second URL segment into `objectKey` with no prefix check, so `DELETE /api/s3/media/x/anything-in-the-bucket` is a valid arbitrary-key delete primitive.\n\nFor contrast, the same package's `listMedia` (line 139) does write `Prefix: mediaRoot ? path.join(mediaRoot, prefix) : prefix`, and `stripMediaRoot` (line 100) exists explicitly to peel `mediaRoot` off keys returned to the client — so the codebase models `mediaRoot` as a security boundary on the read side. The write and delete sides simply forgot to apply it.\n\nThe same misalignment is duplicated in three sibling packages:\n\n* `packages/next-tinacms-dos/src/handlers.ts:115-125` — upload builds `Key: mediaRoot ? path.join(mediaRoot, prefix + filename) : prefix + filename` over attacker-controlled `prefix` (from `req.body.directory`) and `filename` (from `multer`'s `file.originalname`); `path.join` collapses `..` segments, so a `directory` of `../..` plus a chosen `filename` lands at any key in the bucket. Lines 249-278 reproduce the S3 delete-by-second-segment pattern.\n* `packages/next-tinacms-azure/src/handlers.ts:44-95` — `uploadMedia` writes a blob at `path.join(directory, filename)` with both values straight out of `formData`. `next-tinacms-azure` has no `mediaRoot` config at all (`AzureBlobStorageConfig` in `src/types.ts`), so the entire container is writable / deletable by any authorized user.\n* `packages/next-tinacms-cloudinary/src/handlers.ts:193-204` — `deleteAsset` calls `cloudinary.uploader.destroy(public_id)` over the attacker-chosen second segment, deleting any asset in the cloud. The same handler's `listMedia` (line 110) interpolates `mediaListOptions.directory` directly into a Cloudinary search-expression DSL string (`folder=\"${directory}\"`), giving an authorized user full search-expression injection — out of scope for this finding but worth a separate report.\n\n## Reproduction (validated locally)\n\nEnvironment: Node 22, `@aws-sdk/client-s3@3.665.0`, `@aws-sdk/s3-request-presigner@3.665.0`, `aws-sdk-client-mock@4.1.0`. The harness vendors `createMediaHandler` byte-for-byte from `packages/next-tinacms-s3/src/handlers.ts` and runs it against a mocked `S3Client`. The AWS SDK signs the URL identically whether the bucket exists — the resulting URL is the same one a real S3 deployment would hand back to the editor's browser, so it would PUT against a real bucket without further help.\n\nPoC layout under `/Users/admin/joplin_research/tinacms-s3-poc/`:\n\n```\npackage.json (deps only)\nhandler-vendored.mjs (verbatim copy of createMediaHandler, types stripped)\npoc.mjs (four exhibits exercising the handler)\n```\n\nHandler is configured the way the package's `README` documents:\n\n```js\nconst handler = createMediaHandler({\n config: { region: 'us-east-1', credentials: { accessKeyId: '…', secretAccessKey: '…' } },\n bucket: 'editor-media-bucket',\n mediaRoot: 'media', // editors are \"scoped to media/\"\n authorized: async () => true, // typical wiring: TinaCloud verified user\n});\n```\n\nRun:\n\n```bash\n$ cd /Users/admin/tinacms-s3-poc && node poc.mjs\n--- Exhibit A: presigned PUT URL for arbitrary bucket key ---\n HTTP status : 200\n signedUrl : https://editor-media-bucket.s3.us-east-1.amazonaws.com/index.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAFA...\n src : https://editor-media-bucket.s3.us-east-1.amazonaws.com/index.html\n signed Key : /index.html\n RESULT : BYPASS (signed URL writes outside media/)\n\n--- Exhibit B: cross-tenant overwrite via attacker-chosen key ---\n signed Key : /tenant-victim/posts/announcement.mdx\n RESULT : BYPASS (cross-tenant write primitive granted)\n\n--- Exhibit C: arbitrary-key DELETE via [...media] route ---\n HTTP status : 200\n S3 Bucket : editor-media-bucket\n S3 Key : tenant-victim/posts/announcement.mdx\n RESULT : BYPASS (arbitrary-key DELETE granted)\n\n--- Exhibit D: attacker-controlled ?expiresIn ---\n X-Amz-Expires: 604800 sec\n RESULT : BYPASS (URL valid far longer than the 1h default)\n```\n\nVerification, in detail:\n\n1. **Exhibit A** — `GET /api/s3/media?key=index.html`. `mediaRoot=media/` is configured but never consulted on the write path. The handler hands back a SigV4 URL whose path is `/index.html`. PUT'ing any body to that URL would create the bucket-root object `index.html` against the operator's IAM credentials. For a deployment that fronts the bucket with CloudFront / S3-website hosting, this is a one-shot site-defacement primitive (and, for any CSP that trusts the bucket, a one-shot stored-XSS primitive against the published site).\n\n2. **Exhibit B** — same primitive, attacker-chosen key `tenant-victim/posts/announcement.mdx`. In the multi-tenant TinaCloud / TinaCMS Cloud deployment shape (one bucket shared across tenants, each tenant assigned a `mediaRoot/<tenant>/` prefix), a Tenant A editor receives a presigned PUT URL for any Tenant B key. Cross-tenant content tampering with no further bug needed.\n\n3. **Exhibit C** — `DELETE /api/s3/media/anything-here-is-ignored/tenant-victim%2Fposts%2Fannouncement.mdx`. The handler reads the catch-all as `req.query.media = ['anything-here-is-ignored', 'tenant-victim/posts/announcement.mdx']`, destructures to `objectKey = media[1]`, dispatches `DeleteObjectCommand({ Bucket: 'editor-media-bucket', Key: 'tenant-victim/posts/announcement.mdx' })`. The mock confirms the captured command's `Key` is the attacker-supplied string. Real S3 will honour the same call. (The mock-captured call confirms the *handler's* behaviour; the real S3 call requires only `s3:DeleteObject` on the bucket, which the package's `README` instructs operators to grant.)\n\n4. **Exhibit D** — `?expiresIn=604800` (7 days, the SigV4 ceiling). The handler accepts it, the resulting `X-Amz-Expires=604800`. Combined with Exhibit A, an attacker who briefly compromises a CMS session (e.g. via a stolen idle browser session or the dev-server CSRF write described in finding 06) can mint a long-lived offline write primitive — usable for a week with no further interaction with the CMS.\n\nThe four exhibits together: cross-mediaRoot write, cross-tenant write, arbitrary-key delete, and long-lived signed-URL minting.\n\n## Impact\n\n* **Cross-tenant / cross-mediaRoot write.** In any deployment where the bucket holds anything other than this one editor's `mediaRoot` (multi-tenant TinaCloud, shared corporate bucket, bucket-backed static site), an authorized editor obtains write authority over every key the IAM credential can reach. Concretely: defacement of a static site fronted by the bucket; cross-tenant content tampering; replacement of `*.html`, `*.js`, `*.json` assets that downstream consumers trust. (Exhibits A, B; `handlers.ts:67-90`.)\n* **Stored XSS via uploaded HTML.** S3 returns objects with the `Content-Type` the upload supplies. The presigned `PutObjectCommand` URL carries no `Content-Type` constraint — the attacker's `PUT` chooses it. An editor can therefore write an HTML / SVG / JS payload to the bucket-root, and any CDN or origin that serves bucket objects directly to an authenticated SaaS surface inherits stored XSS in that origin.\n* **Arbitrary-key delete (data destruction).** Through `[...media]` the handler treats the second URL segment as a free-form object key. An editor can wipe any object the IAM key can `s3:DeleteObject`, including non-media artefacts like deployment manifests, backups, or other tenants' files. (Exhibit C; `handlers.ts:199-223`.)\n* **Bucket-policy implications.** The `README` (lines 38-90) tells operators to grant the IAM key `s3:PutObject`, `s3:PutObjectAcl`, `s3:DeleteObject`, and `s3:ListBucket` on the *whole* bucket. The README implies the application code constrains the editor's actions to `mediaRoot`. Because the application code does not, every operator who follows the README has — without realising — granted every CMS editor full write/delete on the whole bucket.\n* **Long-lived offline write primitive.** `expiresIn` is attacker-controlled and not capped by the handler. A single GET request mints a URL valid for up to 7 days (AWS SigV4 ceiling). An attacker who briefly compromises a CMS session can stash one such URL and continue writing arbitrary objects long after the session is revoked. (Exhibit D.)\n* **Same-shape exposure on every other first-party media adapter.** `next-tinacms-dos` (DigitalOcean Spaces, S3-compatible), `next-tinacms-azure` (Blob Storage), and `next-tinacms-cloudinary` (Cloudinary) all duplicate the broken pattern. Azure has *no* mediaRoot config at all, so the boundary that `next-tinacms-s3` advertises but doesn't enforce isn't even nominally present in `next-tinacms-azure`.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "next-tinacms-s3"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "23.0.4"
} ]
} ]
}, {
"package" : {
"ecosystem" : "npm",
"name" : "next-tinacms-dos"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "23.0.4"
} ]
} ]
}, {
"package" : {
"ecosystem" : "npm",
"name" : "next-tinacms-azure"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "14.0.4"
} ]
} ]
}, {
"package" : {
"ecosystem" : "npm",
"name" : "next-tinacms-cloudinary"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "26.0.4"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/tinacms/tinacms/security/advisories/GHSA-8mq9-5fw2-5rm4"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-59992"
}, {
"type" : "WEB",
"url" : "https://github.com/tinacms/tinacms/pull/7088"
}, {
"type" : "WEB",
"url" : "https://github.com/tinacms/tinacms/commit/d44558e9b4502d4f4fc2c970d22985339fe2b6ce"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/tinacms/tinacms"
}, {
"type" : "WEB",
"url" : "https://github.com/tinacms/tinacms/releases/tag/next-tinacms-s3@23.0.4"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-639", "CWE-862" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-08-19T21:56:30Z",
"nvd_published_at" : "2026-08-19T22:16:40Z"
}
}