-
Notifications
You must be signed in to change notification settings - Fork 2k
Actions: Add workflow_dispatch and workflow_call input sources for code injection #21660
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
78b7399
f4c3c35
c9ea8d1
c676bdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| ## Overview | ||
|
|
||
| Using string-typed `workflow_call` inputs in GitHub Actions may lead to code injection in contexts like _run:_ or _script:_. | ||
|
|
||
| Inputs declared as `string` should be treated with caution. Although `workflow_call` can only be triggered by other workflows (not directly by external users), the calling workflow may pass untrusted user input as arguments. Since the reusable workflow author has no control over the callers, these inputs may still originate from untrusted data. | ||
|
|
||
|
Comment on lines
+1
to
+6
|
||
| Code injection in GitHub Actions may allow an attacker to exfiltrate any secrets used in the workflow and the temporary GitHub repository authorization token. | ||
|
|
||
| ## Recommendation | ||
|
|
||
| The best practice to avoid code injection vulnerabilities in GitHub workflows is to set the untrusted input value of the expression to an intermediate environment variable and then use the environment variable using the native syntax of the shell/script interpreter (that is, not _${{ env.VAR }}_). | ||
|
|
||
| It is also recommended to limit the permissions of any tokens used by a workflow such as the GITHUB_TOKEN. | ||
|
|
||
| ## Example | ||
|
|
||
| ### Incorrect Usage | ||
|
|
||
| The following example uses a `workflow_call` input directly in a _run:_ step, which allows code injection if the caller passes untrusted data: | ||
|
|
||
| ```yaml | ||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| title: | ||
| description: 'Title' | ||
| type: string | ||
|
|
||
| jobs: | ||
| greet: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - run: | | ||
| echo '${{ inputs.title }}' | ||
| ``` | ||
|
|
||
| ### Correct Usage | ||
|
|
||
| The following example safely uses a `workflow_call` input by passing it through an environment variable: | ||
|
|
||
| ```yaml | ||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| title: | ||
| description: 'Title' | ||
| type: string | ||
|
|
||
| jobs: | ||
| greet: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - env: | ||
| TITLE: ${{ inputs.title }} | ||
| run: | | ||
| echo "$TITLE" | ||
| ``` | ||
|
|
||
| ## References | ||
|
|
||
| - GitHub Security Lab Research: [Keeping your GitHub Actions and workflows secure: Untrusted input](https://securitylab.github.com/research/github-actions-untrusted-input). | ||
| - GitHub Docs: [Security hardening for GitHub Actions](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions). | ||
| - GitHub Docs: [Reusing workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows). | ||
| - Common Weakness Enumeration: [CWE-94: Improper Control of Generation of Code ('Code Injection')](https://cwe.mitre.org/data/definitions/94.html). | ||
| - Common Weakness Enumeration: [CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')](https://cwe.mitre.org/data/definitions/95.html). | ||
| - Common Weakness Enumeration: [CWE-116: Improper Encoding or Escaping of Output](https://cwe.mitre.org/data/definitions/116.html). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /** | ||
| * @name Code injection | ||
| * @description Using unsanitized workflow_call inputs as code allows a calling workflow to | ||
| * pass untrusted user input, leading to code execution. | ||
| * @kind path-problem | ||
| * @problem.severity warning | ||
| * @security-severity 3.0 | ||
| * @precision low | ||
| * @id actions/code-injection/low | ||
| * @tags actions | ||
| * security | ||
| * external/cwe/cwe-094 | ||
| * external/cwe/cwe-095 | ||
| * external/cwe/cwe-116 | ||
| */ | ||
|
|
||
| import actions | ||
| import codeql.actions.security.CodeInjectionQuery | ||
| import CodeInjectionFlow::PathGraph | ||
|
|
||
| from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink | ||
| where lowSeverityCodeInjection(source, sink) | ||
| select sink.getNode(), source, sink, | ||
| "Potential code injection in $@, which may be controlled by a calling workflow.", sink, | ||
| sink.getNode().asExpr().(Expression).getRawExpression() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,22 @@ | ||
| edges | ||
| | .github/workflows/reusable_workflow.yml:22:7:24:4 | Job outputs node [job-output1] | .github/workflows/reusable_workflow.yml:11:17:11:52 | jobs.job1.outputs.job-output1 | provenance | | | ||
| | .github/workflows/reusable_workflow.yml:22:7:24:4 | Job outputs node [job-output2] | .github/workflows/reusable_workflow.yml:13:17:13:52 | jobs.job1.outputs.job-output2 | provenance | | | ||
| | .github/workflows/reusable_workflow.yml:22:21:22:57 | steps.step1.outputs.step-output | .github/workflows/reusable_workflow.yml:22:7:24:4 | Job outputs node [job-output1] | provenance | | | ||
| | .github/workflows/reusable_workflow.yml:23:21:23:63 | steps.step2.outputs.all_changed_files | .github/workflows/reusable_workflow.yml:22:7:24:4 | Job outputs node [job-output2] | provenance | | | ||
| | .github/workflows/reusable_workflow.yml:25:9:31:6 | Run Step: step1 [step-output] | .github/workflows/reusable_workflow.yml:22:21:22:57 | steps.step1.outputs.step-output | provenance | | | ||
| | .github/workflows/reusable_workflow.yml:27:25:27:49 | inputs.config-path | .github/workflows/reusable_workflow.yml:25:9:31:6 | Run Step: step1 [step-output] | provenance | | | ||
| | .github/workflows/reusable_workflow.yml:31:9:33:43 | Uses Step: step2 | .github/workflows/reusable_workflow.yml:23:21:23:63 | steps.step2.outputs.all_changed_files | provenance | | | ||
| nodes | ||
| | .github/workflows/reusable_workflow.yml:11:17:11:52 | jobs.job1.outputs.job-output1 | semmle.label | jobs.job1.outputs.job-output1 | | ||
| | .github/workflows/reusable_workflow.yml:13:17:13:52 | jobs.job1.outputs.job-output2 | semmle.label | jobs.job1.outputs.job-output2 | | ||
| | .github/workflows/reusable_workflow.yml:22:7:24:4 | Job outputs node [job-output1] | semmle.label | Job outputs node [job-output1] | | ||
| | .github/workflows/reusable_workflow.yml:22:7:24:4 | Job outputs node [job-output2] | semmle.label | Job outputs node [job-output2] | | ||
| | .github/workflows/reusable_workflow.yml:22:21:22:57 | steps.step1.outputs.step-output | semmle.label | steps.step1.outputs.step-output | | ||
| | .github/workflows/reusable_workflow.yml:23:21:23:63 | steps.step2.outputs.all_changed_files | semmle.label | steps.step2.outputs.all_changed_files | | ||
| | .github/workflows/reusable_workflow.yml:25:9:31:6 | Run Step: step1 [step-output] | semmle.label | Run Step: step1 [step-output] | | ||
| | .github/workflows/reusable_workflow.yml:27:25:27:49 | inputs.config-path | semmle.label | inputs.config-path | | ||
| | .github/workflows/reusable_workflow.yml:31:9:33:43 | Uses Step: step2 | semmle.label | Uses Step: step2 | | ||
| subpaths | ||
| #select | ||
| | .github/workflows/reusable_workflow.yml:11:17:11:52 | jobs.job1.outputs.job-output1 | .github/workflows/reusable_workflow.yml:27:25:27:49 | inputs.config-path | .github/workflows/reusable_workflow.yml:11:17:11:52 | jobs.job1.outputs.job-output1 | Source | | ||
| | .github/workflows/reusable_workflow.yml:13:17:13:52 | jobs.job1.outputs.job-output2 | .github/workflows/reusable_workflow.yml:31:9:33:43 | Uses Step: step2 | .github/workflows/reusable_workflow.yml:13:17:13:52 | jobs.job1.outputs.job-output2 | Source | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| name: Code Injection via workflow_call | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| title: | ||
| description: "Title string input" | ||
| required: true | ||
| type: string | ||
| count: | ||
| description: "A number input" | ||
| required: false | ||
| type: number | ||
| flag: | ||
| description: "A boolean input" | ||
| required: false | ||
| type: boolean | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| # Vulnerable: string input used directly in run script | ||
| - name: vulnerable string input | ||
| run: | | ||
| echo "${{ inputs.title }}" | ||
|
|
||
| # Not vulnerable: number input (not a string type) | ||
| - name: safe number input | ||
| run: | | ||
| echo "${{ inputs.count }}" | ||
|
|
||
| # Not vulnerable: boolean input (not a string type) | ||
tspascoal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| - name: safe boolean input | ||
| run: | | ||
| echo "${{ inputs.flag }}" | ||
|
|
||
| # Not vulnerable: input passed safely through env var | ||
| - name: safe string input via env | ||
| run: | | ||
| echo "$title" | ||
| env: | ||
| title: ${{ inputs.title }} | ||
Uh oh!
There was an error while loading. Please reload this page.