In 2023 a Microsoft employee published a link to an Azure Storage account in a public GitHub repository for open-source AI models, and the link carried an overly permissive shared access signature (SAS) token for an internal storage account. Wiz found it and reported it on 22 June 2023; Microsoft revoked the token and blocked external access by 24 June. Microsoft says the exposed data included backups of two former employees' workstation profiles and their Teams messages, and no customer data; Wiz reported far wider access. Microsoft's own scanning had flagged the URL but marked it a false positive. Treat every SAS as a bearer credential: scope it to one object, keep it short-lived, prefer user delegation SAS, and triage scanner hits as real secrets.
- A SAS URL is a bearer credential: anyone who has the link has the access it encodes, for as long as it is valid.
- Account-key SAS cannot be revoked one at a time unless tied to a stored access policy; otherwise revocation means rotating the account key.
- Microsoft's rescanning detected the URL in the 'robust-models-transfer' repository, but 'the finding was incorrectly marked as a false positive'.
- Azure Storage's guidance in Microsoft's post: least privilege, 1 hour or less, a revocation plan, monitoring, and a SAS expiration policy.
- Microsoft says there was no vulnerability in Azure Storage or SAS; this was credential handling.
- Wiz's figures (38TB, 'full control' permissions, expiry in 2051, over 30,000 Teams messages) are Wiz's; Microsoft's post does not confirm them.
Incident at a glance
- Dates
- to
- Category
- Misconfiguration
- Organizations
- Microsoft (AI research)
- Wiz (reporter)
- Impact
- A SAS URL with an overly permissive token for an internal storage account sat in a public GitHub repository for AI models. Microsoft says it exposed two former employees' workstation backups and Teams messages, and no customer data.
- How it was found
- Wiz found the URL and reported it to MSRC on 22 June 2023; Microsoft's own rescanning had flagged it earlier but marked it a false positive.
Timeline
- Wiz reports the over-permissive SAS token to MSRC.
- MSRC revokes the token and blocks external access to the storage account.
- MSRC publishes its account and expanded SAS detection in GitHub secret scanning.
| Setting | Value | Note | Source |
|---|---|---|---|
| What leaked | A blob store URL with an overly permissive SAS token for an internal storage account | Shared in a public GitHub repository for open-source AI learning models. | MSRC, 18 September 2023 |
| Exposed, per Microsoft | Two former employees' workstation profile backups and their Teams messages | 'No customer data was exposed.' | MSRC |
| Reported and mitigated | Reported 22 June 2023; mitigated 24 June 2023 | Token revoked and external access to the account blocked. | MSRC |
| Recommended SAS lifetime | 1 hour or less | Have clients request a new SAS when needed. | MSRC, citing Azure Storage |
| Preferred SAS type | User delegation SAS (Microsoft Entra ID) | Maximum 7 days; account and service SAS are signed with the account key. | Microsoft Learn |
| Enforcement | SAS expiration policy with action Block; AllowSharedKeyAccess false | Disabling Shared Key also stops service and account SAS. | Microsoft Learn; Azure CLI reference |
- Run a SAS scanner like the one on this page, and your secret scanner, over public repositories, notebooks, model cards and dataset documentation your organization publishes.
- For each storage account, check
allowSharedKeyAccess,allowBlobPublicAccessand the SAS policy (az storage account show --query "{shared:allowSharedKeyAccess, sas:sasPolicy}"). - Review secret-scanning alerts closed as false positives in the last year, and confirm each was checked by someone who could tell a live credential from a dummy.
- Confirm you can answer, for every SAS you issue, who it was for, what it can do, and how you would revoke it today.
- Check that data shared publicly for research lives in a separate storage account with nothing else in it.
- Applies to
- Azure Storage shared access signatures Account SAS, service SAS and user delegation SAS, as documented on Microsoft Learn in 2026
- Other providers' signed URLs AWS S3 presigned URLs and Google Cloud Storage signed URLs share the bearer-credential risk
Teams that share cloud storage through signed URLs (Azure SAS, AWS presigned URLs, Google Cloud signed URLs), publish data or models from research projects, or run secret scanning and triage its results.
- Part of
- Workflows: Secure a feature, Respond to a vulnerability. Collection: case studies
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Explain why a SAS URL is a bearer credential and how its type decides how it can be revoked.
- Find and grade SAS URLs in published material without exposing the signature.
- Enforce short-lived, scoped SAS at the storage account and remove account-key SAS where possible.
- Separate Microsoft's account of the exposure from the discoverer's figures.
Before you start
Cloud and platform engineers who share storage through signed URLs, ML and research teams who publish data and models, and the teams that run secret scanning
- Know what an Azure Storage account, container and blob are.
- Know what a bearer credential is.
- For the code: Python 3 for the scanner; the Azure CLI and a test subscription to try the policy commands.
Refresh first: Apply a security baseline across AWS, Azure and Google Cloud
Go to the guided practiceHow a research link became a storage-account credential
- Create a SASA researcher creates a SAS URL to share files from an internal storage account.
- Publish itThe URL, including its token, goes into a public GitHub repository for open-source AI models.
- Scanner hit dismissedMicrosoft's rescans of its public repositories detect the URL, but the finding is marked a false positive.
- Someone else uses itWiz finds the URL and uses the token to read the storage account, then reports it to MSRC on 22 June 2023.
- RevokeMSRC revokes the token and blocks external access to the account on 24 June 2023.
On 18 September 2023 the Microsoft Security Response Center (MSRC) described an incident 'involving a Microsoft employee who shared a URL for a blob store in a public GitHub repository while contributing to open-source AI learning models. This URL included an overly-permissive Shared Access Signature (SAS) token for an internal storage account.' Researchers at Wiz found the URL, used the token, and reported it through coordinated disclosure on 22 June 2023. MSRC revoked the token and blocked all external access to the storage account, 'mitigating the issue on June 24th, 2023.'
According to Microsoft, the exposed data 'included backups of two former employees' workstation profiles and internal Microsoft Teams messages of these two employees with their colleagues. No customer data was exposed, and no other internal services were put at risk.' Microsoft was explicit that 'There was no security issue or vulnerability within Azure Storage or the SAS token feature.'
Wiz's own account of the same exposure is much larger, and the two are not reconciled in the public record. This page uses Microsoft's account as the primary source and labels Wiz's figures as Wiz's. Either way, the mechanism is the same and common: a signed URL is a credential, and it was published.
| When | Event | Source |
|---|---|---|
| Before June 2023 | A SAS URL for an internal storage account is published in the 'robust-models-transfer' repository; Microsoft's historical rescans detect it but mark it a false positive. | MSRC |
| 2023-06-22 | Wiz reports the exposure to MSRC under coordinated vulnerability disclosure. | MSRC |
| 2023-06-24 | MSRC revokes the SAS token and prevents all external access to the storage account. | MSRC |
| 2023-09-18 | MSRC and Wiz publish their accounts; MSRC describes expanded GitHub secret scanning detection for over-permissive SAS tokens. | MSRC; Wiz |
A shared access signature is a set of query parameters appended to a storage URL and signed with a key. Azure Storage checks the signature and grants whatever the parameters say: which resources, which operations, from which IP addresses, and until when. There are three kinds. An account SAS and a service SAS are signed with the storage account key; a user delegation SAS is signed with a key obtained through Microsoft Entra ID, and Microsoft recommends it 'when possible for superior security'.
Microsoft Learn is blunt about the properties that matter in an incident: 'The SAS token is not tracked by Azure Storage in any way', and 'It's not possible to audit the generation of SAS tokens.' A stored access policy can revoke a service SAS without touching keys, but 'Stored access policies are not supported for the user delegation SAS or the account SAS.' An account SAS that leaks can only be revoked by rotating the key that signed it, which breaks everything else that key signed. A user delegation SAS expires with its user delegation key, and can never be valid for more than seven days.
MSRC did not publish the exact scope or expiry of the leaked token beyond calling it 'overly-permissive'. Wiz, which found it, reported that the token was 'configured to share the entire storage account', with 'full control' permissions rather than read-only, and valid until 2051; Microsoft's post does not confirm those details. The cloud provider baselines guide (/domains/runtime/cloud-posture/cloud-provider-baselines/) covers turning off Shared Key, which removes account-key SAS altogether.
Microsoft already had two layers of detection. GitHub secret scanning ran a SAS detection provided by Microsoft, which at the time flagged SAS URLs 'pointing to sensitive content, such as VHDs and private cryptographic keys.' Microsoft also ran 'complete historical rescans of all public repositories in Microsoft-owned or affiliated organizations and accounts.' That second system 'detected the specific SAS URL identified by Wiz in the robust-models-transfer repo, but the finding was incorrectly marked as a false positive.'
Microsoft fixed the root cause of that misclassification and expanded the GitHub detection 'to include any SAS token that may have overly-permissive expirations or privileges.' The broader lesson applies to any scanner: detection is half the control. If the triage step cannot tell a live, powerful credential from a harmless one, the scanner's best findings get closed. Triage needs the context a scanner lacks, such as what the token can do and how long it lives, and a closed alert for a credential should record why it was judged harmless. The leaked secret guide (/domains/identity-crypto/secrets/leaked-secret-response/) covers what to do once a finding is confirmed.
The link was published to share material for open-source AI models, which is normal research practice: weights, datasets and checkpoints are large, and a storage link is the easy way to hand them out. The failure was sharing them from a storage account that also held unrelated internal data, with a token whose scope reached that data.
Publish research artifacts from a dedicated storage account, or a dedicated container with a scoped, read-only, short-lived or anonymous-read configuration made on purpose, with nothing else in it. Review what a public link can reach before it is published, the same way a public bucket would be reviewed. The model artifact guide (/domains/ai-emerging/ml-security/model-artifact-security/) covers publishing and consuming model files safely.
The scanner below finds Azure Storage SAS URLs in text, works out whether each is an account, service or user delegation SAS from its parameters (ss and srt mark an account SAS, skoid a user delegation SAS, si a stored access policy), and reports risky permissions, revocation limits and lifetimes over one hour. It prints the resource path and never the sig value. Run with Python 3.14 on a sample file with a fake long-lived account SAS and a 30-minute read-only user delegation SAS, it flagged the first on all three counts and passed the second. Use it alongside your secret scanner, on repositories, notebooks and documentation, and treat every hit as live until proven otherwise.
"""Find Azure Storage SAS URLs in text files and grade them, without printing secrets.
Account SAS (ss, srt) and service SAS are signed with the account key; a user
delegation SAS (skoid) is signed with a Microsoft Entra key. Flags write, delete
or list permissions, and lifetimes beyond a limit. Usage: find_sas.py FILE...
"""
import re
import sys
from datetime import datetime, timezone
from urllib.parse import parse_qs, urlsplit
URL = re.compile(r"https://[a-z0-9]{3,24}\.(?:blob|dfs|file|queue|table)\.core\.windows\.net/[^\s\"'<>]*\bsig=[^\s\"'<>]+")
RISKY = {"w": "write", "d": "delete", "l": "list", "a": "add", "c": "create", "x": "delete version", "y": "permanent delete"}
MAX_HOURS = 1 # MSRC: Azure Storage recommends 1 hour or less
def when(value):
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def grade(url):
q = {k: v[0] for k, v in parse_qs(urlsplit(url).query).items()}
kind = "user delegation SAS" if "skoid" in q else "account SAS" if "ss" in q else "service SAS"
if "si" in q:
kind += " (stored access policy)"
problems = [f"can {RISKY[p]}" for p in q.get("sp", "") if p in RISKY]
if kind == "account SAS":
problems.append("account SAS cannot use a stored access policy: revoking means rotating the key")
elif kind == "service SAS":
problems.append("no stored access policy: revoking means rotating the account key")
if "se" in q:
start = when(q["st"]) if "st" in q else datetime.now(timezone.utc)
hours = (when(q["se"]) - start).total_seconds() / 3600
if hours > MAX_HOURS:
problems.append(f"valid for {hours:,.0f} hours (until {q['se']})")
host_path = url.split("?")[0]
return host_path, kind, problems
if __name__ == "__main__":
found = 0
for path in sys.argv[1:]:
with open(path, encoding="utf-8", errors="replace") as f:
for number, line in enumerate(f, 1):
for match in URL.finditer(line):
found += 1
where, kind, problems = grade(match.group(0))
print(f"{path}:{number}: {where} [{kind}]") # never print the sig
for problem in problems:
print(f" - {problem}")
sys.exit(1 if found else 0)Policy beats discipline. A SAS expiration policy sets an upper limit on SAS validity for every SAS type; its action is Log by default, and Block refuses out-of-policy SAS. Microsoft Learn notes that you may need to rotate both account keys once before the policy can be set, and that Azure File Sync needs at least three days. Disabling Shared Key access stops every request authorized with the account key, including service and account SAS, so only Entra ID and user delegation SAS remain. The commands below follow the current Azure CLI reference; they were checked for shell syntax but not run against an Azure subscription for this page.
# Cap SAS lifetime at 1 hour and refuse longer ones (Block); the default is Log.
az storage account update --name stexample --resource-group rg-example \
--sas-expiration-period 0.01:00:00 --sas-expiration-action Block
# Stop account-key (Shared Key) authorization, which also stops service and
# account SAS; only Microsoft Entra ID and user delegation SAS remain.
az storage account update --name stexample --resource-group rg-example \
--allow-shared-key-access false --allow-blob-public-access false
# Share one blob, read-only, for 30 minutes, signed with Entra ID, not the key.
end=$(date -u -d "+30 minutes" '+%Y-%m-%dT%H:%MZ') # GNU date; macOS: date -u -v+30M ...
az storage blob generate-sas --account-name stexample --container-name models \
--name model-v1.bin --permissions r --expiry "$end" --https-only \
--as-user --auth-mode login --full-uriThe SSDF column names NIST SP 800-218 version 1.1 practices; the ASVS column names ASVS 5.0 requirements.
| Control | SSDF practice or standard | Would it have helped? | Guide |
|---|---|---|---|
| Scope each SAS to one object with the fewest permissions, 1 hour or less | ASVS V13.3.2; MSRC guidance | Yes: a read-only SAS for the shared files would have exposed only them. | /domains/runtime/cloud-posture/cloud-provider-baselines/ |
| Prefer user delegation SAS; disable Shared Key where possible | SSDF PW.9.1 | Yes: no account-key SAS could have been created. | /domains/runtime/cloud-posture/cloud-provider-baselines/ |
| SAS expiration policy with action Block | ASVS V13.3.4 | Yes: a decades-long SAS would have been refused, if Wiz's account of the expiry is right. | /domains/runtime/cloud-posture/cloud-security-posture/ |
| Secret scanning with push protection, and triage that records why a hit is harmless | SSDF PO.3.2, PS.1.1 | Yes: Microsoft's own scanner found it; triage closed it. | /domains/identity-crypto/secrets/leaked-secret-response/ |
| Separate storage accounts for data meant to be public | SSDF PO.5.1 | Yes: a leaked token would reach only public material. | /domains/ai-emerging/ml-security/model-artifact-security/ |
| A revocation plan per SAS type: stored access policy, key rotation, user delegation key expiry | MSRC guidance | Limits impact: shortens how long a leaked token works. | /domains/identity-crypto/secrets/secrets-lifecycle/ |
| Storage logging and alerting on SAS use from unexpected places | ASVS V16.3.2 | Shortens detection; it would not prevent the leak. | /domains/runtime/detection/security-event-logging/ |
- A signed URL is a credential. Review it like one before it goes into a repository, notebook or README.
- Scope, lifetime and revocation decide the damage: one object, read-only, an hour, and a way to revoke it.
- Prefer user delegation SAS and turn off Shared Key; then no one can mint an unrevocable account-key SAS.
- A scanner that finds the secret is only as good as the triage that follows; audit closed alerts.
- Keep what you publish in a place that holds nothing else.
Microsoft's post does not state the token's permissions, scope or expiry beyond 'overly-permissive', and it does not give the volume of data in the storage account. Wiz reported that the token shared the entire storage account, including 38TB of private files, with 'full control' permissions, valid until 2051, and that the exposed data included over 30,000 internal Teams messages from 359 Microsoft employees; those figures are Wiz's and are not confirmed by Microsoft, whose account names two former employees' data. The public record does not say whether anyone other than Wiz used the token, how long the URL had been public, or exactly when the false-positive decision was made.
Guided practice
You run security for an AI team that publishes models and datasets. A review turns up the items below. Decide the risk and the action for each. Treat the facts as true.
Supplied scenario material
L1: A README in a public repo links to a blob container with an account SAS: ss=b, srt=sco, sp=rwdl, st=2026-01-10, se=2040-01-01.
L2: A notebook in a public repo uses a user delegation SAS for one blob: sr=b, sp=r, 30 minutes, expired last week.
L3: A service SAS with si=public-read (a stored access policy) on a container that holds only released model files.
L4: A secret-scanning alert for an Azure SAS in a dataset card was closed in March as 'test data'; no reason recorded.
L5: The storage account behind L1 also holds the team's training logs and evaluation results.1. Contain L1
What is L1's exposure and how do you revoke it?
Expected result and reasoning
Read, write, delete and list on the account's blob service for 14 years, signed with the account key. An account SAS cannot use a stored access policy, so rotate the signing key (after moving legitimate users to Entra ID or new scoped SAS), then review storage logs for use of the token. Remove the link from the repository and its history.
2. Judge L2
Does L2 need action?
Expected result and reasoning
Low risk: read-only, one blob, expired. Remove it from the notebook anyway and use a mechanism that issues a fresh SAS on demand.
3. Judge L3
Is L3 acceptable?
Expected result and reasoning
Reasonable if the container holds only public material: the stored access policy allows revocation without key rotation. Confirm the policy's permissions are read and list only, and that nothing private is ever written there.
4. Reopen L4
What do you do about L4?
Expected result and reasoning
Treat it as live until proven otherwise: parse the SAS, check its scope and expiry, revoke if powerful, and require a recorded reason for closing credential alerts.
5. Fix the design
What changes stop a repeat of L1 and L5?
Expected result and reasoning
A dedicated account for published artifacts, Shared Key disabled on internal accounts, a SAS expiration policy with Block, and pre-publish scanning of READMEs, notebooks and dataset cards.
This is a fictional inventory. Revoking an account SAS by rotating keys can break other clients that use the key, so plan the rotation before you do it.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- MSRC: Microsoft mitigated exposure of internal information in a storage account due to overly-permissive SAS token. 18 September 2023.
- Microsoft Learn: Grant limited access to data with shared access signatures (SAS). Updated 27 February 2026.
- Microsoft Learn: Configure an expiration policy for shared access signatures
- Microsoft Learn: Create a user delegation SAS (REST API)
- Azure CLI reference: az storage account
- Azure CLI reference: az storage blob generate-sas
- Wiz (third-party research, discoverer): 38TB of data accidentally exposed by Microsoft AI researchers. 18 September 2023.
Reference information
- SDLC phase
- Maintenance
- ASVS 5.0.0 requirements
- v5.0.0-13.3.2 · v5.0.0-13.3.4