CI-Story: How We Found Critical Vulnerabilities in StoryBook Project

user profile
Head of Security Research

StoryBook is an extremely popular open-source frontend framework that allows the building of isolated UI components and pages. Several factors indicate its popularity:

  • It is one of the most popular projects on GitHub, with 75k stars
  • More than 16 million downloads per month for just its React NPM package
  • More than 1800 contributors to the project
  • According to the StoryBook website, the product is used by major companies, such as GitHub, Airbnb, Mozilla, Salesforce, Lyft, Atlassian, and more.
  • Wide variety of plugins and integrations

Cycode found several vulnerabilities in its GitHub Actions development pipeline that may have allowed any user on the internet to run arbitrary code on its build server. This Remote Code Execution (RCE) capability could have allowed attackers to insert malicious code into the popular project or exfiltrate sensitive tokens.

We consider these vulnerabilities critical due to the following reasons:

  • The project is open-source, allowing any user on the internet to exploit it without any user intervention.
  • As we’ll see, exploiting the vulnerabilities is relatively easy.
  • The project is highly popular, so inserting any malicious code into it can potentially cause a massive supply-chain attack on many victims.

Fortunately, we discovered it only thirteen days after it had been inserted into the main branch, allowing minimal time for potential attackers to exploit these vulnerabilities.

Vulnerability Explained

Like other popular open-source projects, StoryBook is maintained on GitHub and uses several CI/CD systems with many customized development workflows. In their case, it is GitHub Actions and CircleCI.

One of the workflows, trigger-circle-ci-workflow, triggers the CircleCI pipeline from GitHub Actions, so it must have permission to access build secrets and uses the permissive trigger: pull_request_target. As we’ll cover in the post-exploitation, it also extends the threat and exploitation scenario to CircleCI.

The workflow has two vulnerabilities:

  1. The first job in the workflow references the branch name inside the script without sanitizing it. It happens in the following line: export BRANCH=${{ github.event.pull_request.head.ref }}.
    The branch name can be configured by any user and may contain malicious scripts.
  2. Subsequent jobs in the workflow reference the first job’s output through ${{ needs.get-branch.outputs.branch }} without sanitizing it, which may cause the same effect as the first vulnerability.

These two issues aren’t related and could compromise the build server independently.

Attack Flow

First, fork the project. Then, create any change in the project (for example, adding a space to the README file) and commit the change into a new beautiful branch name: alex${IFS};fi;echo${IFS}pwned;

Then push this to the official repository as a forked pull request. That triggers the vulnerable workflow, with elevated permissions and possible access to sensitive secrets.

We didn’t perform this attack on the official repository. Still, we reproduced it in our lab environment to verify that such a pull request would yield the following output in the build logs:

We can see it managed to print the pwned text, just like the script we embedded into the branch name instructed it!

GitHub limits the branch name to 255 chars, which leaves us more than enough room to fetch a malicious script, give it proper permissions, run it, and get complete code execution capabilities on the build server!

The second vulnerability is caused by referencing ${{ needs.get-branch.outputs.branch }} in several places around the workflow. With a similar attack path and a few tweaks in the payload, we could also exploit it. 

Post-Exploitation Consequences

Exfiltrating CircleCI Token

By exploiting the second vulnerability alone, we could easily exfiltrate the CIRCLE_CI_TOKEN. Build secrets are exposed in the step they are used. One of the jobs for the workflow which has access to the secret is trigger-ci-tests. This is the job description from the workflow:

trigger-ci-tests:
  runs-on: ubuntu-latest
  needs: get-branch
  if: github.event_name == 'pull_request_target' && 
      github.event.pull_request.draft == true && 
      !contains(github.event.pull_request.labels.*.name, 'ci:pr') && 
      !contains(github.event.pull_request.labels.*.name, 'ci:merged') && 
      !contains(github.event.pull_request.labels.*.name, 'ci:daily')
  steps:
    - name: Trigger draft PR tests
      run: >
        curl -X POST --location "https://circleci.com/api/v2/project/gh/storybookjs/storybook/pipeline" \
          -H "Content-Type: application/json" \
          -H "Circle-Token: $CIRCLE_CI_TOKEN" \
          -d '{
                "branch": "${{ needs.get-branch.outputs.branch }}",
                "parameters": {
                  "workflow": "ci"
                }
              }'
      env:
        CIRCLE_CI_TOKEN: ${{ secrets.CIRCLE_CI_TOKEN }}

We chose this build specifically because it doesn’t demand labels on PRs, which requires user intervention.

Using Job Token

While there isn’t a direct usage of GITHUB_TOKEN in the workflow, it is passed to every build. The GitHub security team explains here – “the repository token, whether it is referenced or not, and any referenced secrets can be harvested by a determined attacker”.

So, assuming attackers exploit the branch injection vulnerability, get complete code execution capability by downloading and installing their script, and extracting the GITHUB_TOKEN, they can use it to commit subtle changes to the repository and hide malicious code. This code insertion can trigger standard CI procedures, be compiled into a package, and be downloaded by millions of users without knowing it contains backdoors or malicious code.

We showcased an additional interesting exploitation technique in our previous blog – it is possible to extract not only the secrets for the specific workflow (the CircleCI token in this case) but all other repository and organization secrets. By looking at the repository, they could be privileged Personal Access Tokens (PAT), Netlify tokens, and more.

Conclusion

Don’t let the ease of the fix confuse you. The StoryBook vulnerability was introduced on 07 Nov 2022 and, for 21 days, exposed its development infrastructure to any user on the internet. Software supply chain attacks are on the rise, and due to the increased complexity of CI/CD systems, we anticipate seeing additional, prominent projects having similar vulnerabilities.

Timeline

  • 07 Nov 2022 – The vulnerability was introduced to the repository
  • 20 Nov 2022 – Vulnerability disclosed to Chromatic (In-charge of maintaining StoryBook project)
  • 21 Nov  2022 – Chromatic acknowledged the vulnerability and started working on the fix
  • 28 Nov 2022 – Fix was introduced, closing the attack surface (ffb855)

How Can Cycode Help You?

Cycode’s platform secures your software supply chain by providing complete visibility into enterprise DevOps tools and infrastructure. With a simple GitHub integration, Cycode’s platform will detect the vulnerabilities mentioned above and additional configuration issues in your GitHub Actions workflows and help you remediate them.

Such violations for the mentioned vulnerable workflow will look like the following: