Skip to content

ShellScriptInjection

Shell script vulnerable to script injection.

Defined by ScriptInjectionRule which supports workflows, actions in the "Default" ruleset along with JSScriptInjection.

Description

Using ${{ }} in shell scripts is vulnerable to script injection. Script injection is when a user can control part of the script, and can inject arbitrary code. In most cases this is a security vulnerability, but at the very least it's a bug. All user input must be sanitized before being executed as shell script.

The simplest way to achieve this is using environment variables to pass data as inputs to run: scripts. Shells know how to handle them: ${XXX}. With environment variables data travels in memory, rather than becoming part of the executable code.

References:

Compliant examples

Compliant example #1

Capturing the input in an environment variable prevents shell injection.

The output is as expected:

Warning: Quotation mark (") needs a pair.

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - name: "Produce some output"
        id: producer
        run: |
          echo 'result=Warning: Quotation mark (") needs a pair.' >> "${GITHUB_OUTPUT}"

      - name: "Consume some output"
        env:
          RESULT: ${{ steps.producer.outputs.result }}
        run: echo "${RESULT}"

Compliant example #2

A note on GitHub variables vs contexts. There are a few examples where using ${{ github.* }} would result in an unsafe script, for example:

- run: cp "${{ github.workspace }}" "${{ runner.temp }}"

Instead of introducing an env: section like this:

- env:
    WS: ${{ github.workspace }}
    RT: ${{ runner.temp }}
  run: cp -r "${WS}" "${RT}"
consider using the ${GITHUB_*} and ${RUNNER_*} environment variables as shown in the example.

Compare:


An exception to this might be when there's a project-specific path that is appended and used multiple times:

- env:
    WS: ${{ github.workspace }}/some/thing
    RT: ${{ runner.temp }}/other/place
  run: |
    unzip "${WS}/foo.zip" -d "${RT}"
    rm -rf "${WS}"
    mv "${RT}" "${WS}"

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: cp -r "${GITHUB_WORKSPACE}" "${RUNNER_TEMP}"

Non-compliant examples

Non-compliant example #1

Directly using the output in the shell script is vulnerable to script injection. Depending on the actual contents of the result

  • in the best case, the script fails,
  • in normal cases, the output is just wrong,
  • in the worst case, this could lead to arbitrary code execution.

In this example, the output is:

/home/runner/work/_temp/d3ddaaab-5e34-4eb3-be73-4e830012fe4e.sh: line 1: syntax error near unexpected token `)'
Error: Process completed with exit code 2.
because after resolving the ${{ ... }} expression, the script becomes:
echo "Warning: Quotation mark (") needs a pair."
where
echo "Warning: Quotation mark ("
is correct, but the remaining code after is meaningless for shells: ) needs a pair.".

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - name: "Produce some output"
        id: producer
        run: |
          echo 'result=Warning: Quotation mark (") needs a pair.' >> "${GITHUB_OUTPUT}"

      - name: "Consume some output directly"
        run: echo "${{ steps.producer.outputs.result }}"

  • Line 11: Step["Consume some output directly"] in Job[example] shell script contains GitHub Expressions.

Non-compliant example #2

Directly using the output in the shell script is vulnerable to script injection.

In this example, the output is:

{add:one,remove:[two,three]}
instead of
{"add":"one","remove":["two","three"]}
because after resolving the ${{ ... }} expression, the script becomes:
echo "{"add":"one","remove":["two","three"]}"
which looks OK at first glance, but shells can actually understand it differently than expected (spaces added for clarity):
echo "{" add ":" one "," remove ":[" two "," three "]}"

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - name: "Produce some output"
        id: producer
        run: |
          echo 'result={"add":"one","remove":["two","three"]}' >> "${GITHUB_OUTPUT}"

      - name: "Consume some output directly"
        run: echo "${{ steps.producer.outputs.result }}"

  • Line 11: Step["Consume some output directly"] in Job[example] shell script contains GitHub Expressions.