mirror of
https://github.com/github/codeql.git
synced 2026-04-26 09:15:12 +02:00
Add support for composite actions
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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 +
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 }}'
|
||||
Reference in New Issue
Block a user