Add support for composite actions

This commit is contained in:
jarlob
2023-04-06 22:53:59 +02:00
parent baefeab2d1
commit 9c7eecf547
4 changed files with 184 additions and 72 deletions

View File

@@ -10,16 +10,62 @@ import javascript
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
*/
module Actions {
/** A YAML node in a GitHub Actions workflow file. */
/** A YAML node in a GitHub Actions workflow or custom action file. */
private class Node extends YamlNode {
Node() {
this.getLocation()
.getFile()
.getRelativePath()
.regexpMatch("(^|.*/)\\.github/workflows/.*\\.y(?:a?)ml$")
exists(File f |
f = this.getLocation().getFile() and
(
f.getRelativePath().regexpMatch("(^|.*/)\\.github/workflows/.*\\.y(?:a?)ml$")
or
f.getBaseName() = "action.yml"
)
)
}
}
/**
* A custom action. This is a mapping at the top level of an Actions YAML action file.
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions.
*/
class Action extends Node, YamlDocument, YamlMapping {
/** Gets the `runs` mapping. */
Runs getRuns() { result = this.lookup("runs") }
}
/**
* An `runs` mapping in a custom action YAML.
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs
*/
class Runs extends StepsContainer {
Action action;
Runs() { action.lookup("runs") = this }
/** Gets the action that this `runs` mapping is in. */
Action getAction() { result = action }
}
/**
* The parent class of the class that can contain `steps` mappings. (`Job` or `Runs` currently.)
*/
abstract class StepsContainer extends YamlNode, YamlMapping {
/** Gets the sequence of `steps` within this YAML node. */
YamlSequence getSteps() { result = this.lookup("steps") }
}
/**
* A `using` mapping in a custom action YAML.
*/
class Using extends YamlNode, YamlScalar {
Runs runs;
Using() { runs.lookup("using") = this }
/** Gets the `runs` mapping that this `using` mapping is in. */
Runs getRuns() { result = runs }
}
/**
* An Actions workflow. This is a mapping at the top level of an Actions YAML workflow file.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
@@ -109,7 +155,7 @@ module Actions {
* An Actions job within a workflow.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
*/
class Job extends YamlNode, YamlMapping {
class Job extends StepsContainer {
string jobId;
Workflow workflow;
@@ -136,9 +182,6 @@ module Actions {
/** Gets the step at the given index within this job. */
Step getStep(int index) { result.getJob() = this and result.getIndex() = index }
/** Gets the sequence of `steps` within this job. */
YamlSequence getSteps() { result = this.lookup("steps") }
/** Gets the `env` mapping in this job. */
YamlMapping getEnv() { result = this.lookup("env") }
@@ -184,15 +227,17 @@ module Actions {
*/
class Step extends YamlNode, YamlMapping {
int index;
Job job;
StepsContainer parent;
Step() { this = job.getSteps().getElement(index) }
Step() { this = parent.getSteps().getElement(index) }
/** Gets the 0-based position of this step within the sequence of `steps`. */
int getIndex() { result = index }
/** Gets the job this step belongs to. */
Job getJob() { result = job }
Job getJob() { result = parent.(Job) }
Runs getRuns() { result = parent.(Runs) }
/** Gets the value of the `uses` field in this step, if any. */
Uses getUses() { result.getStep() = this }

View File

@@ -103,70 +103,122 @@ private predicate isExternalUserControlledWorkflowRun(string context) {
)
}
from YamlNode node, string injection, string context, Actions::On on
/**
* The env variable name in `${{ env.name }}`
* is where the external user controlled value was assigned to.
*/
bindingset[injection]
predicate isEnvTainted(Actions::Env env, string injection, string context) {
Actions::getEnvName(injection) = env.getName() and
Actions::getASimpleReferenceExpression(env) = context
}
/**
* Holds if the `run` contains any expression interpolation `${{ e }}`.
* Sets `context` to the initial untrusted value assignment in case of `${{ env... }}` interpolation
*/
predicate isRunInjectable(Actions::Run run, string injection, string context) {
Actions::getASimpleReferenceExpression(run) = injection and
(
injection = context
or
exists(Actions::Env env | isEnvTainted(env, injection, context))
)
}
/**
* Holds if the `actions/github-script` contains any expression interpolation `${{ e }}`.
* Sets `context` to the initial untrusted value assignment in case of `${{ env... }}` interpolation
*/
predicate isScriptInjectable(Actions::Script script, string injection, string context) {
exists(Actions::Step step, Actions::Uses uses |
script.getWith().getStep() = step and
uses.getStep() = step and
uses.getGitHubRepository() = "actions/github-script" and
Actions::getASimpleReferenceExpression(script) = injection and
(
injection = context
or
exists(Actions::Env env | isEnvTainted(env, injection, context))
)
)
}
from YamlNode node, string injection, string context
where
(
exists(Actions::Run run |
node = run and
Actions::getASimpleReferenceExpression(run) = injection and
run.getStep().getJob().getWorkflow().getOn() = on and
(
injection = context
or
exists(Actions::Env env |
Actions::getEnvName(injection) = env.getName() and
Actions::getASimpleReferenceExpression(env) = context
)
exists(Actions::Using u, Actions::Runs runs |
u.getValue() = "composite" and
u.getRuns() = runs and
(
exists(Actions::Run run |
isRunInjectable(run, injection, context) and
node = run and
run.getStep().getRuns() = runs
)
)
or
exists(Actions::Script script, Actions::Step step, Actions::Uses uses |
node = script and
script.getWith().getStep().getJob().getWorkflow().getOn() = on and
script.getWith().getStep() = step and
uses.getStep() = step and
uses.getGitHubRepository() = "actions/github-script" and
Actions::getASimpleReferenceExpression(script) = injection and
(
injection = context
or
exists(Actions::Env env |
Actions::getEnvName(injection) = env.getName() and
Actions::getASimpleReferenceExpression(env) = context
)
or
exists(Actions::Script script |
node = script and
script.getWith().getStep().getRuns() = runs and
isScriptInjectable(script, injection, context)
)
) and
(
isExternalUserControlledIssue(context) or
isExternalUserControlledPullRequest(context) or
isExternalUserControlledReview(context) or
isExternalUserControlledComment(context) or
isExternalUserControlledGollum(context) or
isExternalUserControlledCommit(context) or
isExternalUserControlledDiscussion(context) or
isExternalUserControlledWorkflowRun(context)
)
)
or
exists(Actions::On on |
(
exists(Actions::Run run |
isRunInjectable(run, injection, context) and
node = run and
run.getStep().getJob().getWorkflow().getOn() = on
)
or
exists(Actions::Script script |
node = script and
script.getWith().getStep().getJob().getWorkflow().getOn() = on and
isScriptInjectable(script, injection, context)
)
) and
(
exists(on.getNode("issues")) and
isExternalUserControlledIssue(context)
or
exists(on.getNode("pull_request_target")) and
isExternalUserControlledPullRequest(context)
or
exists(on.getNode("pull_request_review")) and
(isExternalUserControlledReview(context) or isExternalUserControlledPullRequest(context))
or
exists(on.getNode("pull_request_review_comment")) and
(isExternalUserControlledComment(context) or isExternalUserControlledPullRequest(context))
or
exists(on.getNode("issue_comment")) and
(isExternalUserControlledComment(context) or isExternalUserControlledIssue(context))
or
exists(on.getNode("gollum")) and
isExternalUserControlledGollum(context)
or
exists(on.getNode("push")) and
isExternalUserControlledCommit(context)
or
exists(on.getNode("discussion")) and
isExternalUserControlledDiscussion(context)
or
exists(on.getNode("discussion_comment")) and
(isExternalUserControlledDiscussion(context) or isExternalUserControlledComment(context))
or
exists(on.getNode("workflow_run")) and
isExternalUserControlledWorkflowRun(context)
)
) and
(
exists(on.getNode("issues")) and
isExternalUserControlledIssue(context)
or
exists(on.getNode("pull_request_target")) and
isExternalUserControlledPullRequest(context)
or
exists(on.getNode("pull_request_review")) and
(isExternalUserControlledReview(context) or isExternalUserControlledPullRequest(context))
or
exists(on.getNode("pull_request_review_comment")) and
(isExternalUserControlledComment(context) or isExternalUserControlledPullRequest(context))
or
exists(on.getNode("issue_comment")) and
(isExternalUserControlledComment(context) or isExternalUserControlledIssue(context))
or
exists(on.getNode("gollum")) and
isExternalUserControlledGollum(context)
or
exists(on.getNode("push")) and
isExternalUserControlledCommit(context)
or
exists(on.getNode("discussion")) and
isExternalUserControlledDiscussion(context)
or
exists(on.getNode("discussion_comment")) and
(isExternalUserControlledDiscussion(context) or isExternalUserControlledComment(context))
or
exists(on.getNode("workflow_run")) and
isExternalUserControlledWorkflowRun(context)
)
select node,
"Potential injection from the ${ " + injection +

View File

@@ -62,3 +62,4 @@
| .github/workflows/workflow_run.yml:14:12:14:77 | echo '$ ... ame }}' | Potential injection from the ${ github.event.workflow_run.head_commit.committer.name }, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:15:12:15:62 | echo '$ ... nch }}' | Potential injection from the ${ github.event.workflow_run.head_branch }, which may be controlled by an external user. |
| .github/workflows/workflow_run.yml:16:12:16:78 | echo '$ ... ion }}' | Potential injection from the ${ github.event.workflow_run.head_repository.description }, which may be controlled by an external user. |
| action.yml:14:12:14:50 | echo '$ ... ody }}' | Potential injection from the ${ github.event.comment.body }, which may be controlled by an external user. |

View File

@@ -0,0 +1,14 @@
name: 'test'
description: 'test'
branding:
icon: 'test'
color: 'test'
inputs:
test:
description: test
required: false
default: 'test'
runs:
using: "composite"
steps:
- run: echo '${{ github.event.comment.body }}'