---
key: "case-study-microsoft-sas-token"
title: "Case study: Microsoft AI researchers' over-permissive SAS token (2023)"
url: "https://softwaresecurity.com/domains/runtime/cloud-posture/case-study-microsoft-sas-token/"
api: "https://softwaresecurity.com/api/v1/articles/case-study-microsoft-sas-token.json"
domain: "runtime"
topic: "Runtime, cloud \u0026 APIs"
type: "Case study"
identifiers: ["CWE-732","CWE-798","A02:2025","A01:2025","V13.3.2","V13.3.4"]
tags: ["Access control","Secrets management","Cloud security","Incident response","Models and training data"]
stages: ["operate","respond"]
roles: ["platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Case study: Microsoft AI researchers' over-permissive SAS token (2023)

How an over-permissive Azure Storage SAS token in a public repository for AI models exposed internal Microsoft data in 2023, and how to scope, scan for and revoke signed URLs.

Canonical URL: https://softwaresecurity.com/domains/runtime/cloud-posture/case-study-microsoft-sas-token/

## In short

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

- Date: 2023-06-22 to 2023-09-18
- 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

1. 2023-06-22: Wiz reports the over-permissive SAS token to MSRC.
2. 2023-06-24: MSRC revokes the token and blocks external access to the storage account.
3. 2023-09-18: MSRC publishes its account and expanded SAS detection in GitHub secret scanning.

## Recommended defaults

| 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 |

## Verify it

1. 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.
2. For each storage account, check `allowSharedKeyAccess`, `allowBlobPublicAccess` and the SAS policy (`az storage account show --query "{shared:allowSharedKeyAccess, sas:sasPolicy}"`).
3. 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.
4. Confirm you can answer, for every SAS you issue, who it was for, what it can do, and how you would revoke it today.
5. Check that data shared publicly for research lives in a separate storage account with nothing else in it.

## 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.

Audience: 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

Before you start:

- 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.

Time: About 25 minutes: 8 to read and 17 for the exercise

## A link to AI training material that opened a storage account

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.

## Timeline



| 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 |

## What a SAS token is, and why this one was dangerous

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.

## Found by the scanner, closed as a false positive

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.

## Why AI research data needs its own boundary

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.

## Find SAS URLs and grade them without printing the secret

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.

```python
"""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)

```

## Enforce short, scoped SAS at the account

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.

```bash
# 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-uri

```

## Which controls would have stopped or limited it?

The 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/ |

## Lessons that still apply



- 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.

## What the public evidence does not establish

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 material

```text
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?

Worked answer: 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?

Worked answer: 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?

Worked answer: 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?

Worked answer: 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?

Worked answer: 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.

Deliverable: A table of L1 to L5 with risk, action and owner, and the three account-level settings you will enforce.

Scope: 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.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [MSRC: Microsoft mitigated exposure of internal information in a storage account due to overly-permissive SAS token](https://www.microsoft.com/en-us/msrc/blog/2023/09/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)](https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview). Updated 27 February 2026.
- [Microsoft Learn: Configure an expiration policy for shared access signatures](https://learn.microsoft.com/en-us/azure/storage/common/sas-expiration-policy)
- [Microsoft Learn: Create a user delegation SAS (REST API)](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas)
- [Azure CLI reference: az storage account](https://learn.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest)
- [Azure CLI reference: az storage blob generate-sas](https://learn.microsoft.com/en-us/cli/azure/storage/blob?view=azure-cli-latest#az-storage-blob-generate-sas)
- [Wiz (third-party research, discoverer): 38TB of data accidentally exposed by Microsoft AI researchers](https://www.wiz.io/blog/38-terabytes-of-private-data-accidentally-exposed-by-microsoft-ai-researchers). 18 September 2023.