GHSA-HHPH-8536-GFQ5
Vulnerability from github – Published: 2026-10-20 18:20 – Updated: 2026-10-20 18:20Summary
When 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.
Verified at commit 5a3746aaed49761e171c2cbfe793267c959829fd (uploadfs 1.27.0 in the apostrophe monorepo).
Details
Root cause: a credential-bearing client URL is reused as the public base URL. The chain:
- Credential-embedded client: azure.js#L123-L125 —
new BlobServiceClient("https://<account>.blob.core.windows.net?<SAS query string>")wherecluster.keyis the full SAS string includingsig - Leak point: azure.js#L513-L520 —
getUrl()takesgetContainerClient(...).getBlobClient('').urlasbaseUrl; SDK v12 derived clients inherit the SAS query string, sobaseUrlalready contains?sv=...&sig=... - Public exposure: attachment/index.js#L694 —
attachment.url()=uploadfs.getUrl() + pathfor every rendered attachment link; attachment/index.js#L1503 —getBrowserData()also shipsuploadsUrlto every logged-in editor session - 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
Attack 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.
Suggested remediation:
1. 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.
2. 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.
PoC
Full 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:
npm install @azure/storage-blob@12 && node poc.js- The script constructs
new BlobServiceClient("https://myaccount.blob.core.windows.net?sv=...&sig=FAKESIGNATURE")exactly likecreateContainer(), then evaluates thegetUrl()logic (getContainerClient('uploads-container').getBlobClient('').url) and asserts the resulting base URL still contains thesigparameter. - Observed output (exit code 0):
baseUrl: https://myaccount.blob.core.windows.net/uploads-container/?sv=2021-06-08&ss=b&srt=co&sp=rwdlac&se=2099-01-01&sig=FAKESIGNATURE
attachment 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
The 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.
Impact
- 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. - 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.
Reported by Kimi Security Team (bug-report@moonshot.ai).
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "uploadfs"
},
"ranges": [
{
"events": [
{
"introduced": "1.25.0"
},
{
"fixed": "1.28.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-113652"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-522"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-20T18:20:35Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## 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=...\u0026sig=...`), and Apostrophe\u0027s `attachment.url()` prepends it to every public attachment link \u2014 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) \u2014 `new BlobServiceClient(\"https://\u003caccount\u003e.blob.core.windows.net?\u003cSAS query string\u003e\")` 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) \u2014 `getUrl()` takes `getContainerClient(...).getBlobClient(\u0027\u0027).url` as `baseUrl`; SDK v12 derived clients inherit the SAS query string, so `baseUrl` already contains `?sv=...\u0026sig=...`\n- Public exposure: [attachment/index.js#L694](https://github.com/apostrophecms/apostrophe/blob/5a3746aaed49761e171c2cbfe793267c959829fd/packages/apostrophe/modules/@apostrophecms/attachment/index.js#L694) \u2014 `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) \u2014 `getBrowserData()` also ships `uploadsUrl` to every logged-in editor session\n- Companion bug: the blob path is appended **after** the query string (`/container/?sv=...\u0026sig=XXX/attachments/id-name`), so these URLs are also broken \u2014 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 \u2014 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(\u0027\u0027).url); u.search = \u0027\u0027;` (or compose `accountUrl + \u0027/\u0027 + 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\u0027s exact two steps with the declared dependency `@azure/storage-blob@12`:\n\n1. `npm install @azure/storage-blob@12 \u0026\u0026 node poc.js`\n2. The script constructs `new BlobServiceClient(\"https://myaccount.blob.core.windows.net?sv=...\u0026sig=FAKESIGNATURE\")` exactly like `createContainer()`, then evaluates the `getUrl()` logic (`getContainerClient(\u0027uploads-container\u0027).getBlobClient(\u0027\u0027).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\u0026ss=b\u0026srt=co\u0026sp=rwdlac\u0026se=2099-01-01\u0026sig=FAKESIGNATURE\nattachment URL as served to visitors: https://myaccount.blob.core.windows.net/uploads-container/?sv=2021-06-08\u0026ss=b\u0026srt=co\u0026sp=rwdlac\u0026se=2099-01-01\u0026sig=FAKESIGNATURE/attachments/abc123-photo.jpg\n```\n\nThe produced URL both leaks the token (`sv`\u2026`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 \u2014 commonly granted `sp=rwdlac` (read/write/delete/list/ACL) \u2014 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).",
"id": "GHSA-hhph-8536-gfq5",
"modified": "2026-10-20T18:20:35Z",
"published": "2026-10-20T18:20:35Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-hhph-8536-gfq5"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350"
},
{
"type": "PACKAGE",
"url": "https://github.com/apostrophecms/apostrophe"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.