{"id":"CVE-2026-113652","revision":null,"records":[{"revision":"aff0f2eb8b334cedbda62917ef4ff3c5","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-113652","aliases":[],"title":"ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)","description":"ApostropheCMS is an open-source Node.js content management system. From 1.25.0 until 1.28.0, uploadfs Azure backend leaks account-level SAS token into every public attachment URL. When uploadfs is configured with the Azure backend and sas: true, createContainer() builds the BlobServiceClient from a URL that embeds the full SAS query string, and Azure SDK v12 derived clients inherit that query string. This issue is fixed in version 1.28.0.","state":0,"publishedAt":"2026-10-20T16:26:21.986+00:00","modifiedAt":"2026-10-21T15:53:38.704+00:00","affected":[{"ecosystem":"Vendor product","name":"apostrophe","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"apostrophecms\",\n                    \"product\": \"apostrophe\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 4.33.0\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"},{"ecosystem":"Vendor product","name":"uploadfs","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"apostrophecms\",\n                    \"product\": \"uploadfs\",\n                    \"versions\": [\n                        {\n                            \"version\": \">= 1.25.0, < 1.28.0\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L","score":5.6,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-hhph-8536-gfq5","https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce","https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350","https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-113652","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-113652","identifiers":["CVE-2026-113652"]},"retained":false,"historicalAffected":[]},{"revision":"a6fb09ff5ac04b94b1fe59c6e8458ac0","observedAt":"2026-11-07T04:34:09.414+00:00","statement":{"source":"euvd","id":"EUVD-2026-105180","aliases":["CVE-2026-113652","GHSA-hhph-8536-gfq5"],"title":"ApostropheCMS is an open-source Node.js content management system. From 1.25.0 until 1.28.0, uploadfs Azure backend leaks account-level SAS token into every public attachment URL. When uploadfs is configured with the Azure backend and sas: …","description":"ApostropheCMS is an open-source Node.js content management system. From 1.25.0 until 1.28.0, uploadfs Azure backend leaks account-level SAS token into every public attachment URL. When uploadfs is configured with the Azure backend and sas: true, createContainer() builds the BlobServiceClient from a URL that embeds the full SAS query string, and Azure SDK v12 derived clients inherit that query string. This issue is fixed in version 1.28.0.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L","score":5.6,"severity":null}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-hhph-8536-gfq5","https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce","https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350","https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-105180","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Oct 20, 2026, 4:26:21 PM","dateUpdated":"Oct 21, 2026, 3:53:38 PM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-105180","identifiers":["EUVD-2026-105180","CVE-2026-113652","GHSA-hhph-8536-gfq5"]},"retained":false,"historicalAffected":[]},{"revision":"1d38b9ce6e92422cbc4fb89d9e649605","observedAt":"2026-10-25T05:17:18.597+00:00","statement":{"source":"nvd","id":"CVE-2026-113652","aliases":[],"title":"ApostropheCMS is an open-source Node.js content management system. From 1.25.0 until 1.28.0, uploadfs Azure backend leaks account-level SAS token into every public attachment URL. When uploadfs is configured with the Azure backend and sas: …","description":"ApostropheCMS is an open-source Node.js content management system. From 1.25.0 until 1.28.0, uploadfs Azure backend leaks account-level SAS token into every public attachment URL. When uploadfs is configured with the Azure backend and sas: true, createContainer() builds the BlobServiceClient from a URL that embeds the full SAS query string, and Azure SDK v12 derived clients inherit that query string. This issue is fixed in version 1.28.0.","state":0,"publishedAt":"2026-10-20T17:17:36.08+00:00","modifiedAt":"2026-10-21T16:17:23.97+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L","score":5.6,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce","https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350","https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0","https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-hhph-8536-gfq5"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-113652","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-200","CWE-522"],"productCriteria":[],"sourceDates":{"published":"2026-10-20T17:17:36.080","lastModified":"2026-10-21T16:17:23.970"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-113652","identifiers":["CVE-2026-113652"]},"retained":false,"historicalAffected":[]},{"revision":"abe668aa0b3340a3a149b0e22094bf44","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-hhph-8536-gfq5","aliases":["CVE-2026-113652"],"title":"ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)","description":"## Summary\n\nWhen uploadfs is configured with the Azure backend and `sas: true` (a supported configuration introduced in uploadfs 1.25.0), `createContainer()` builds the `BlobServiceClient` from a URL that embeds the full SAS query string, and Azure SDK v12 derived clients inherit that query string. As a result, `getUrl()` returns a base URL carrying the complete account/container SAS signature (`?sv=...&sig=...`), and Apostrophe's `attachment.url()` prepends it to every public attachment link — broadcasting a container-level SAS credential (commonly `sp=rwdlac`, full read/write/delete/list) to every site visitor, including anonymous ones. Anyone who can see an attachment URL can read, overwrite, delete, and list every blob in the container for the SAS lifetime. A companion functional bug (blob path appended after the query string, producing broken URLs) shows this code path was never exercised end to end. Shared-key mode (default) and non-Azure backends are not affected.\n\nVerified at commit `5a3746aaed49761e171c2cbfe793267c959829fd` (uploadfs 1.27.0 in the apostrophe monorepo).\n\n## Details\n\nRoot cause: a credential-bearing client URL is reused as the public base URL. The chain:\n\n- Credential-embedded client: [azure.js#L123-L125](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/uploadfs/lib/storage/azure.js#L123-L125) — `new BlobServiceClient(\"https://<account>.blob.core.windows.net?<SAS query string>\")` where `cluster.key` is the full SAS string including `sig`\n- Leak point: [azure.js#L513-L520](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/uploadfs/lib/storage/azure.js#L513-L520) — `getUrl()` takes `getContainerClient(...).getBlobClient('').url` as `baseUrl`; SDK v12 derived clients inherit the SAS query string, so `baseUrl` already contains `?sv=...&sig=...`\n- Public exposure: [attachment/index.js#L694](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/apostrophe/modules/@apostrophecms/attachment/index.js#L694) — `attachment.url()` = `uploadfs.getUrl() + path` for every rendered attachment link; [attachment/index.js#L1503](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/apostrophe/modules/@apostrophecms/attachment/index.js#L1503) — `getBrowserData()` also ships `uploadsUrl` to every logged-in editor session\n- Companion bug: the blob path is appended **after** the query string (`/container/?sv=...&sig=XXX/attachments/id-name`), so these URLs are also broken — evidence the 1.25.0 code path was never tested\n\nAttack surface: every public page rendering attachment URLs (images, download links), plus the `uploadsUrl` delivered to editor sessions. The leak is passive and unconditional — no action required from the victim beyond serving a page. The credential further propagates through HTML source, logs, Referer headers, and CDN/proxy access logs.\n\n**Suggested remediation:**\n1. Strip the query string before building public URLs: `const u = new URL(blob.svc.getContainerClient(...).getBlobClient('').url); u.search = '';` (or compose `accountUrl + '/' + container` manually), and append the blob path to the pathname, not after the query.\n2. Document that SAS mode must use least privilege (read-only, ideally `sr=b` per-blob), and prefer generating short-lived per-blob read-only signed URLs via `generateBlobSASQueryParameters` instead of embedding a service-level SAS in permanent public URLs.\n\n## PoC\n\nFull PoC posted as the first comment below (`poc.js` + README; offline, no Azure account needed). It replicates the library's exact two steps with the declared dependency `@azure/storage-blob@12`:\n\n1. `npm install @azure/storage-blob@12 && node poc.js`\n2. The script constructs `new BlobServiceClient(\"https://myaccount.blob.core.windows.net?sv=...&sig=FAKESIGNATURE\")` exactly like `createContainer()`, then evaluates the `getUrl()` logic (`getContainerClient('uploads-container').getBlobClient('').url`) and asserts the resulting base URL still contains the `sig` parameter.\n3. Observed output (exit code 0):\n\n```\nbaseUrl: https://myaccount.blob.core.windows.net/uploads-container/?sv=2021-06-08&ss=b&srt=co&sp=rwdlac&se=2099-01-01&sig=FAKESIGNATURE\nattachment URL as served to visitors: https://myaccount.blob.core.windows.net/uploads-container/?sv=2021-06-08&ss=b&srt=co&sp=rwdlac&se=2099-01-01&sig=FAKESIGNATURE/attachments/abc123-photo.jpg\n```\n\nThe produced URL both leaks the token (`sv`…`sig`) and demonstrates the broken path placement. Cross-checking `attachment/index.js` confirms this URL reaches public pages unmodified.\n\n## Impact\n\n- **What it is:** insufficiently protected credentials (CWE-522) leading to information exposure (CWE-200). A container-level SAS token — commonly granted `sp=rwdlac` (read/write/delete/list/ACL) — is disclosed to every site visitor including anonymous users, valid for the SAS lifetime (often years). Holders can read, overwrite, delete, and enumerate every attachment in the container; the library applies no narrowing.\n- **Who is impacted:** Apostrophe sites using uploadfs with the Azure backend and `sas: true` (introduced in 1.25.0; present through 1.27.0). Default shared-key mode and other backends are unaffected. When the SAS is provisioned read-only, the impact reduces to unauthenticated read/download of all attachments.\n\n---\n\nReported by **Kimi Security Team** (bug-report@moonshot.ai).","state":0,"publishedAt":"2026-10-20T18:20:35+00:00","modifiedAt":"2026-10-20T18:30:03.590258+00:00","affected":[{"ecosystem":"npm","name":"uploadfs","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"1.25.0"},{"kind":"fixed","version":"1.28.0"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"MODERATE"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-hhph-8536-gfq5","https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce","https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350","https://github.com/apostrophecms/apostrophe","https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0"],"sourceUrl":"https://github.com/advisories/GHSA-hhph-8536-gfq5","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-hhph-8536-gfq5","identifiers":["GHSA-hhph-8536-gfq5","CVE-2026-113652"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-08T09:05:07.1353409+00:00","complete":true,"records":1769,"lastAttemptAt":"2026-11-08T09:05:07.1353409+00:00","lastSuccessAt":"2026-11-08T09:05:07.314358+00:00","error":null,"lastVerifiedAt":"2026-11-08T09:05:07.3144897+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-eaa64dac63caa79c8d4905619258c41c51b39e461b996124e1a21b5e85e7e738-e58a079eb6f76d5949b871c295ad9e8f78e928765ff9950581a38d46375b9852","complete":true,"records":421096,"lastAttemptAt":"2026-11-08T09:04:21.5548423+00:00","lastSuccessAt":"2026-11-08T07:59:14.3145447+00:00","error":"HttpRequestException","lastVerifiedAt":"2026-11-08T07:59:14.338564+00:00","snapshotPublishedAt":"2026-11-08T07:27:32+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-ce0df89a405747cda5b7c61ae0b48fb2","complete":true,"records":13633,"lastAttemptAt":"2026-11-08T09:05:09.4085736+00:00","lastSuccessAt":"2026-11-08T05:08:40.263+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:08:40.263+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-08T09:05:07.1353409+00:00","complete":true,"records":1096,"lastAttemptAt":"2026-11-08T09:05:07.1353409+00:00","lastSuccessAt":"2026-11-08T09:05:07.4545479+00:00","error":null,"lastVerifiedAt":"2026-11-08T09:05:07.4545481+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-2404c35233cd476289f9792161a850e3","complete":true,"records":13645,"lastAttemptAt":"2026-11-08T09:05:08.6143971+00:00","lastSuccessAt":"2026-11-08T08:59:23.805+00:00","error":null,"lastVerifiedAt":"2026-11-08T08:59:23.805+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-0eb4d6dde62dffacd040ad992d4fd5a65cd7d0dc5f5f4cfa8b41967fc6f9ca5f","complete":true,"records":9914,"lastAttemptAt":"2026-11-08T09:05:07.0080286+00:00","lastSuccessAt":"2026-11-06T20:10:36.6450506+00:00","error":null,"lastVerifiedAt":"2026-11-08T09:05:07.0080286+00:00","snapshotPublishedAt":"2026-11-06T20:02:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-fde804134ecdda0ddd5ff5c26ee1fff5a6fa659311f7ef2e51c6ad54964db0e2","complete":true,"records":1971,"lastAttemptAt":"2026-11-08T09:05:03.7558577+00:00","lastSuccessAt":"2026-11-06T19:03:17.0399903+00:00","error":"HttpRequestException","lastVerifiedAt":"2026-11-08T08:58:58.22321+00:00","snapshotPublishedAt":"2026-11-06T18:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-335633e78a15c9cde10a6025895b5617717087fb945489ae7e15d377bacf34a9","complete":true,"records":3055,"lastAttemptAt":"2026-11-08T09:05:07.0409305+00:00","lastSuccessAt":"2026-11-06T16:03:00.7615789+00:00","error":null,"lastVerifiedAt":"2026-11-08T09:05:07.0409305+00:00","snapshotPublishedAt":"2026-11-06T16:02:48+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-8efbf882841c7fda0cba0c3b55677f4781f461b96ab83577ad2701c4eaf88b1e","complete":true,"records":231293,"lastAttemptAt":"2026-11-08T09:05:06.6586785+00:00","lastSuccessAt":"2026-11-08T08:01:14.837451+00:00","error":null,"lastVerifiedAt":"2026-11-08T09:05:06.6586785+00:00","snapshotPublishedAt":"2026-11-08T07:33:16+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}