Vulnerability GHSA-hhph-8536-gfq5
Summary
ApostropheCMS uploadfs Azure backend leaks account-level SAS token into every public attachment URL (sas: true)
Details
Summary
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:
- Strip the query string before building public URLs:
const u = new URL(blob.svc.getContainerClient(...).getBlobClient('').url); u.search = '';(or composeaccountUrl + '/' + containermanually), and append the blob path to the pathname, not after the query. - Document that SAS mode must use least privilege (read-only, ideally
sr=bper-blob), and prefer generating short-lived per-blob read-only signed URLs viagenerateBlobSASQueryParametersinstead 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.
Related Vulnerabilities
Other vulnerabilities affecting the same packages