yaml.github-actions.security.github-script-injection.github-script-injection

profile photo of semgrepsemgrep
Author
unknown
Download Count*

Using variable interpolation ${{...}} with github context data in a actions/github-script's script: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

Run Locally

Run in CI

Defintion

rules:
  - id: github-script-injection
    languages:
      - yaml
    message: "Using variable interpolation `${{...}}` with `github` context data in
      a `actions/github-script`'s `script:` step could allow an attacker to
      inject their own code into the runner. This would allow them to steal
      secrets and code. `github` context data can have arbitrary user input and
      should be treated as untrusted. Instead, use an intermediate environment
      variable with `env:` to store the data and use the environment variable in
      the `run:` script. Be sure to use double-quotes the environment variable,
      like this: \"$ENVVAR\"."
    metadata:
      category: security
      cwe:
        - "CWE-94: Improper Control of Generation of Code ('Code Injection')"
      owasp:
        - A03:2021 - Injection
      references:
        - https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#understanding-the-risk-of-script-injections
        - https://securitylab.github.com/research/github-actions-untrusted-input/
        - https://github.com/actions/github-script
      technology:
        - github-actions
      cwe2022-top25: true
      subcategory:
        - vuln
      likelihood: HIGH
      impact: HIGH
      confidence: HIGH
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      vulnerability_class:
        - Code Injection
    patterns:
      - pattern-inside: "steps: [...]"
      - pattern-inside: |
          uses: $ACTION
          ...
      - pattern-inside: |
          with:
            ...
            script: ...
            ...
      - pattern: "script: $SHELL"
      - metavariable-regex:
          metavariable: $ACTION
          regex: actions/github-script@.*
      - metavariable-pattern:
          language: generic
          metavariable: $SHELL
          patterns:
            - pattern-either:
                - pattern: ${{ github.event.issue.title }}
                - pattern: ${{ github.event.issue.body }}
                - pattern: ${{ github.event.pull_request.title }}
                - pattern: ${{ github.event.pull_request.body }}
                - pattern: ${{ github.event.comment.body }}
                - pattern: ${{ github.event.review.body }}
                - pattern: ${{ github.event.review_comment.body }}
                - pattern: ${{ github.event.pages. ... .page_name}}
                - pattern: ${{ github.event.head_commit.message }}
                - pattern: ${{ github.event.head_commit.author.email }}
                - pattern: ${{ github.event.head_commit.author.name }}
                - pattern: ${{ github.event.commits ... .author.email }}
                - pattern: ${{ github.event.commits ... .author.name }}
                - pattern: ${{ github.event.pull_request.head.ref }}
                - pattern: ${{ github.event.pull_request.head.label }}
                - pattern: ${{ github.event.pull_request.head.repo.default_branch }}
                - pattern: ${{ github.head_ref }}
                - pattern: ${{ github.event.inputs ... }}
    severity: ERROR

Examples

github-script-injection.test.yaml

name: test-script-run

on:
- push

jobs:
  script-run:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Run script 1
        uses: actions/github-script@v6
        if: steps.report-diff.outputs.passed == 'true'
        with:
          # ruleid: github-script-injection
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('/tmp/file.txt', {encoding: 'utf8'});

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '${{ github.event.pull_request.title }}' + body
            })

            return true;

      - name: Run script 2
        uses: actions/github-script@latest
        with:
          # ruleid: github-script-injection
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('/tmp/${{ github.event.issue.title }}.txt', {encoding: 'utf8'});

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Thanks for reporting!'
            })

            return true;

      - name: Ok script 1
        uses: not-github/custom-action@latest
        with:
          # ok: github-script-injection
          script: |
            return ${{ github.event.issue.title }};

      - name: Ok script 2
        uses: actions/github-script@latest
        with:
          # ok: github-script-injection
          script: |
            console.log('${{ github.event.workflow_run.artifacts_url }}');

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Thanks for reporting!'
            })

            return true;