Merge remote-tracking branch 'origin/main' into nickrolfe/misspelling

This commit is contained in:
Nick Rolfe
2022-05-12 16:10:27 +01:00
124 changed files with 22038 additions and 5967 deletions

View File

@@ -2,53 +2,47 @@
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Using user-controlled input in GitHub Actions may lead to
code injection in contexts like <i>run:</i> or <i>script:</i>.
</p>
<p>
Code injection in GitHub Actions may allow an attacker to
exfiltrate the temporary GitHub repository authorization token.
The token might have write access to the repository, allowing an attacker
to use the token to make changes to the repository.
</p>
</overview>
<recommendation>
<p>
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.
</p>
<p>
It is also recommended to limit the permissions of any tokens used
by a workflow such as the the GITHUB_TOKEN.
</p>
</recommendation>
<example>
<p>
The following example lets a user inject an arbitrary shell command:
</p>
<sample src="examples/comment_issue_bad.yml" />
<p>
The following example uses shell syntax to read
the environment variable and will prevent the attack:
</p>
<sample src="examples/comment_issue_good.yml" />
</example>
<references>
<li>GitHub Security Lab Research: <a href="https://securitylab.github.com/research/github-actions-untrusted-input">Keeping your GitHub Actions and workflows secure: Untrusted input</a>.</li>
<li>GitHub Docs: <a href="https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions">Security hardening for GitHub Actions</a>.</li>
<li>GitHub Docs: <a href="https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token">Permissions for the GITHUB_TOKEN</a>.</li>
</references>
</qhelp>

View File

@@ -3,7 +3,8 @@
* @description Using user-controlled GitHub Actions contexts like `run:` or `script:` may allow a malicious
* user to inject code into the GitHub action.
* @kind problem
* @problem.severity error
* @problem.severity warning
* @security-severity 9.3
* @precision high
* @id js/actions/injection
* @tags actions
@@ -12,7 +13,7 @@
*/
import javascript
import experimental.semmle.javascript.Actions
import semmle.javascript.Actions
bindingset[context]
private predicate isExternalUserControlledIssue(string context) {
@@ -22,14 +23,18 @@ private predicate isExternalUserControlledIssue(string context) {
bindingset[context]
private predicate isExternalUserControlledPullRequest(string context) {
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*title\\b") or
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*body\\b") or
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*label\\b") or
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*default_branch\\b") or
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*ref\\b")
exists(string reg |
reg =
[
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*title\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*body\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*label\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*default_branch\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*ref\\b",
]
|
context.regexpMatch(reg)
)
}
bindingset[context]
@@ -51,18 +56,20 @@ private predicate isExternalUserControlledGollum(string context) {
bindingset[context]
private predicate isExternalUserControlledCommit(string context) {
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*message\\b") or
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*message\\b") or
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*email\\b") or
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*name\\b") or
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*author\\s*\\.\\s*email\\b") or
context
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*author\\s*\\.\\s*name\\b") or
context.regexpMatch("\\bgithub\\s*\\.\\s*head_ref\\b")
exists(string reg |
reg =
[
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*message\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*message\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*name\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*author\\s*\\.\\s*email\\b",
"\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*author\\s*\\.\\s*name\\b",
"\\bgithub\\s*\\.\\s*head_ref\\b"
]
|
context.regexpMatch(reg)
)
}
bindingset[context]
@@ -73,7 +80,7 @@ private predicate isExternalUserControlledDiscussion(string context) {
from Actions::Run run, string context, Actions::On on
where
run.getAReferencedExpression() = context and
run.getASimpleReferenceExpression() = context and
run.getStep().getJob().getWorkflow().getOn() = on and
(
exists(on.getNode("issues")) and

View File

@@ -0,0 +1,6 @@
---
category: newQuery
---
* The `js/actions/injection` query has been added. It highlights GitHub Actions workflows that may allow an
attacker to execute arbitrary code in the workflow.
The query previously existed an experimental query.

View File

@@ -13,7 +13,7 @@
*/
import javascript
import experimental.semmle.javascript.Actions
import semmle.javascript.Actions
/**
* An action step that doesn't contain `actor` or `label` check in `if:` or
@@ -78,7 +78,7 @@ class ProbableJob extends Actions::Job {
/**
* An action step that doesn't contain `actor` or `label` check in `if:` or
*/
class ProbablePullRequestTarget extends Actions::On, Actions::MappingOrSequenceOrScalar {
class ProbablePullRequestTarget extends Actions::On, YAMLMappingLikeNode {
ProbablePullRequestTarget() {
exists(YAMLNode prtNode |
// The `on:` is triggered on `pull_request_target`
@@ -88,7 +88,7 @@ class ProbablePullRequestTarget extends Actions::On, Actions::MappingOrSequenceO
not exists(prtNode.getAChild())
or
// or has the filter, that is something else than just [labeled]
exists(Actions::MappingOrSequenceOrScalar prt, Actions::MappingOrSequenceOrScalar types |
exists(YAMLMappingLikeNode prt, YAMLMappingLikeNode types |
types = prt.getNode("types") and
prtNode = prt and
(

View File

@@ -1,316 +1,4 @@
/**
* Libraries for modeling GitHub Actions workflow files written in YAML.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
*/
import javascript
/**
* Libraries for modeling GitHub Actions workflow files written in YAML.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
*/
module Actions {
/** A YAML node in a GitHub Actions workflow file. */
private class Node extends YAMLNode {
Node() {
this.getLocation()
.getFile()
.getRelativePath()
.matches(["experimental/Security/CWE-094/.github/workflows/%", ".github/workflows/%"])
}
}
/**
* Actions are quite flexible in parsing YAML.
*
* For example:
* ```
* on: pull_request
* ```
* and
* ```
* on: [pull_request]
* ```
* and
* ```
* on:
* pull_request:
* ```
*
* are equivalent.
*/
class MappingOrSequenceOrScalar extends YAMLNode {
MappingOrSequenceOrScalar() {
this instanceof YAMLMapping
or
this instanceof YAMLSequence
or
this instanceof YAMLScalar
}
YAMLNode getNode(string name) {
exists(YAMLMapping mapping |
mapping = this and
result = mapping.lookup(name)
)
or
exists(YAMLSequence sequence, YAMLNode node |
sequence = this and
sequence.getAChildNode() = node and
node.eval().toString() = name and
result = node
)
or
exists(YAMLScalar scalar |
scalar = this and
scalar.getValue() = name and
result = scalar
)
}
int getElementCount() {
exists(YAMLMapping mapping |
mapping = this and
result = mapping.getNumChild() / 2
)
or
exists(YAMLSequence sequence |
sequence = this and
result = sequence.getNumChild()
)
or
exists(YAMLScalar scalar |
scalar = this and
result = 1
)
}
}
/**
* 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.
*/
class Workflow extends Node, YAMLDocument, YAMLMapping {
/** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */
YAMLMapping getJobs() { result = this.lookup("jobs") }
/** Gets the name of the workflow file. */
string getFileName() { result = this.getFile().getBaseName() }
/** Gets the `on:` in this workflow. */
On getOn() { result = this.lookup("on") }
/** Gets the job within this workflow with the given job ID. */
Job getJob(string jobId) { result.getWorkflow() = this and result.getId() = jobId }
}
/**
* An Actions On trigger within a workflow.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#on.
*/
class On extends YAMLNode, MappingOrSequenceOrScalar {
Workflow workflow;
On() { workflow.lookup("on") = this }
Workflow getWorkflow() { result = workflow }
}
/**
* 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 {
string jobId;
Workflow workflow;
Job() { this = workflow.getJobs().lookup(jobId) }
/**
* Gets the ID of this job, as a string.
* This is the job's key within the `jobs` mapping.
*/
string getId() { result = jobId }
/**
* Gets the ID of this job, as a YAML scalar node.
* This is the job's key within the `jobs` mapping.
*/
YAMLString getIdNode() { workflow.getJobs().maps(result, this) }
/** Gets the human-readable name of this job, if any, as a string. */
string getName() { result = this.getNameNode().getValue() }
/** Gets the human-readable name of this job, if any, as a YAML scalar node. */
YAMLString getNameNode() { result = this.lookup("name") }
/** 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 workflow this job belongs to. */
Workflow getWorkflow() { result = workflow }
/** Gets the value of the `if` field in this job, if any. */
JobIf getIf() { result.getJob() = this }
}
/**
* An `if` within a job.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idif.
*/
class JobIf extends YAMLNode, YAMLScalar {
Job job;
JobIf() { job.lookup("if") = this }
/** Gets the step this field belongs to. */
Job getJob() { result = job }
}
/**
* A step within an Actions job.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idsteps.
*/
class Step extends YAMLNode, YAMLMapping {
int index;
Job job;
Step() { this = job.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 }
/** Gets the value of the `uses` field in this step, if any. */
Uses getUses() { result.getStep() = this }
/** Gets the value of the `run` field in this step, if any. */
Run getRun() { result.getStep() = this }
/** Gets the value of the `if` field in this step, if any. */
StepIf getIf() { result.getStep() = this }
}
/**
* An `if` within a step.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsif.
*/
class StepIf extends YAMLNode, YAMLScalar {
Step step;
StepIf() { step.lookup("if") = this }
/** Gets the step this field belongs to. */
Step getStep() { result = step }
}
/**
* A `uses` field within an Actions job step, which references an action as a reusable unit of code.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsuses.
*
* For example:
* ```
* uses: actions/checkout@v2
* ```
* TODO: Does not currently handle local repository references, e.g. `.github/actions/action-name`.
*/
class Uses extends YAMLNode, YAMLScalar {
Step step;
/** The owner of the repository where the Action comes from, e.g. `actions` in `actions/checkout@v2`. */
string repositoryOwner;
/** The name of the repository where the Action comes from, e.g. `checkout` in `actions/checkout@v2`. */
string repositoryName;
/** The version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
string version;
Uses() {
step.lookup("uses") = this and
// Simple regular expression to split up an Action reference `owner/repo@version` into its components.
exists(string regexp | regexp = "([^/]+)/([^/@]+)@(.+)" |
repositoryOwner = this.getValue().regexpCapture(regexp, 1) and
repositoryName = this.getValue().regexpCapture(regexp, 2) and
version = this.getValue().regexpCapture(regexp, 3)
)
}
/** Gets the step this field belongs to. */
Step getStep() { result = step }
/** Gets the owner and name of the repository where the Action comes from, e.g. `actions/checkout` in `actions/checkout@v2`. */
string getGitHubRepository() { result = repositoryOwner + "/" + repositoryName }
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
string getVersion() { result = version }
}
/**
* A `with` field within an Actions job step, which references an action as a reusable unit of code.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswith.
*
* For example:
* ```
* with:
* arg1: 1
* arg2: abc
* ```
*/
class With extends YAMLNode, YAMLMapping {
Step step;
With() { step.lookup("with") = this }
/** Gets the step this field belongs to. */
Step getStep() { result = step }
}
/**
* A `ref:` field within an Actions `with:` specific to `actions/checkout` action.
*
* For example:
* ```
* uses: actions/checkout@v2
* with:
* ref: ${{ github.event.pull_request.head.sha }}
* ```
*/
class Ref extends YAMLNode, YAMLString {
With with;
Ref() { with.lookup("ref") = this }
With getWith() { result = with }
}
/**
* A `run` field within an Actions job step, which runs command-line programs using an operating system shell.
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
*/
class Run extends YAMLNode, YAMLString {
Step step;
Run() { step.lookup("run") = this }
/** Gets the step that executes this `run` command. */
Step getStep() { result = step }
/**
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this `run` command.
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
*/
string getAReferencedExpression() {
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
// not just the last (greedy match) or first (reluctant match).
// TODO: This only handles expression strings that refer to contexts.
// It does not handle operators within the expression.
result =
this.getValue()
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\.\\-]+\\s*\\}\\}", _, _)
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\.\\-]+)\\s*\\}\\}", 1)
}
}
/** DEPRECATED: Use `semmle.javascript.Actions` instead. */
deprecated module Actions {
import semmle.javascript.Actions::Actions
}