mirror of
https://github.com/github/codeql.git
synced 2026-04-30 03:05:15 +02:00
Merge remote-tracking branch 'origin/main' into nickrolfe/misspelling
This commit is contained in:
@@ -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>
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user