mirror of
https://github.com/github/codeql.git
synced 2026-04-30 19:26:02 +02:00
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//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>
|
||||
|
||||
</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>
|
||||
|
||||
</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>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @name Expression injection in Actions
|
||||
* @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
|
||||
* @precision high
|
||||
* @id js/actions/injection
|
||||
* @tags actions
|
||||
* security
|
||||
* external/cwe/cwe-094
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import experimental.semmle.javascript.Actions
|
||||
|
||||
bindingset[context]
|
||||
private predicate isExternalUserControlledIssue(string context) {
|
||||
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*issue\\s*\\.\\s*title\\b") or
|
||||
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*issue\\s*\\.\\s*body\\b")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
bindingset[context]
|
||||
private predicate isExternalUserControlledReview(string context) {
|
||||
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*review\\s*\\.\\s*body\\b") or
|
||||
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*review_comment\\s*\\.\\s*body\\b")
|
||||
}
|
||||
|
||||
bindingset[context]
|
||||
private predicate isExternalUserControlledComment(string context) {
|
||||
context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*comment\\s*\\.\\s*body\\b")
|
||||
}
|
||||
|
||||
bindingset[context]
|
||||
private predicate isExternalUserControlledGollum(string context) {
|
||||
context
|
||||
.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pages(?:\\[[0-9]\\]|\\s*\\.\\s*\\*)+\\s*\\.\\s*page_name\\b")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
from Actions::Run run, string context, Actions::On on
|
||||
where
|
||||
run.getAReferencedExpression() = context and
|
||||
run.getStep().getJob().getWorkflow().getOn() = on 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_comment")) or exists(on.getNode("pull_request_review"))) and
|
||||
isExternalUserControlledReview(context)
|
||||
or
|
||||
(exists(on.getNode("issue_comment")) or exists(on.getNode("pull_request_target"))) and
|
||||
isExternalUserControlledComment(context)
|
||||
or
|
||||
exists(on.getNode("gollum")) and
|
||||
isExternalUserControlledGollum(context)
|
||||
or
|
||||
exists(on.getNode("pull_request_target")) and
|
||||
isExternalUserControlledCommit(context)
|
||||
)
|
||||
select run,
|
||||
"Potential injection from the " + context +
|
||||
" context, which may be controlled by an external user."
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
|
||||
<p>
|
||||
|
||||
Combining <i>pull_request_target</i> workflow trigger with an explicit checkout
|
||||
of an untrusted pull request is a dangerous practice
|
||||
that may lead to repository compromise.
|
||||
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
|
||||
The best practice is to handle the potentially untrusted pull request
|
||||
via the <i>pull_request</i> trigger so that it is isolated in
|
||||
an unprivileged environment. The workflow processing the pull request
|
||||
should then store any results like code coverage or failed/passed tests
|
||||
in artifacts and exit. The following workflow then starts on <i>workflow_run</i>
|
||||
where it is granted write permission to the target repository and access to
|
||||
repository secrets, so that it can download the artifacts and make
|
||||
any necessary modifications to the repository or interact with third party services
|
||||
that require repository secrets (e.g. API tokens).
|
||||
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
|
||||
<p>
|
||||
|
||||
The following example allows unauthorized repository modification
|
||||
and secrets exfiltration:
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/pull_request_target_bad.yml" />
|
||||
|
||||
<p>
|
||||
|
||||
The following example uses two workflows to handle potentially untrusted
|
||||
pull request in a secure manner. The receive_pr.yml is triggered first:
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/receive_pr.yml" />
|
||||
<p>The comment_pr.yml is triggered after receive_pr.yml completes:</p>
|
||||
<sample src="examples/comment_pr.yml" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>GitHub Security Lab Research: <a href="https://securitylab.github.com/research/github-actions-preventing-pwn-requests">Keeping your GitHub Actions and workflows secure: Preventing pwn requests</a>.</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @name Checkout of untrusted code in trusted context
|
||||
* @description Workflows triggered on `pull_request_target` have read/write access to the base repository and access to secrets.
|
||||
* By explicitly checking out and running the build script from a fork the untrusted code is running in an environment
|
||||
* that is able to push to the base repository and to access secrets.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @precision low
|
||||
* @id js/actions/pull_request_target
|
||||
* @tags actions
|
||||
* security
|
||||
* external/cwe/cwe-094
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import experimental.semmle.javascript.Actions
|
||||
|
||||
/**
|
||||
* Action step that doesn't contain `actor` or `label` check in `if:` or
|
||||
* the check requires manual analysis.
|
||||
*/
|
||||
class ProbableStep extends Actions::Step {
|
||||
// some simplistic checks to eleminate likely false positives:
|
||||
ProbableStep() {
|
||||
// no if at all
|
||||
not exists(this.getIf().getValue())
|
||||
or
|
||||
// needs manual analysis if there is OR
|
||||
this.getIf().getValue().matches("%||%")
|
||||
or
|
||||
// labels can be assigned by owners only
|
||||
not exists(
|
||||
this.getIf()
|
||||
.getValue()
|
||||
.regexpFind("\\bcontains\\s*\\(\\s*github\\s*\\.\\s*event\\s*\\.\\s*(?:issue|pull_request)\\s*\\.\\s*labels\\b",
|
||||
_, _)
|
||||
) and
|
||||
not exists(
|
||||
this.getIf()
|
||||
.getValue()
|
||||
.regexpFind("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*label\\s*\\.\\s*name\\s*==", _, _)
|
||||
) and
|
||||
// actor check means only the user is able to run it
|
||||
not exists(this.getIf().getValue().regexpFind("\\bgithub\\s*\\.\\s*actor\\s*==", _, _))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action job that doesn't contain `actor` or `label` check in `if:` or
|
||||
* the check requires manual analysis.
|
||||
*/
|
||||
class ProbableJob extends Actions::Job {
|
||||
// some simplistic checks to eleminate likely false positives:
|
||||
ProbableJob() {
|
||||
// no if at all
|
||||
not exists(this.getIf().getValue())
|
||||
or
|
||||
// needs manual analysis if there is OR
|
||||
this.getIf().getValue().matches("%||%")
|
||||
or
|
||||
// labels can be assigned by owners only
|
||||
not exists(
|
||||
this.getIf()
|
||||
.getValue()
|
||||
.regexpFind("\\bcontains\\s*\\(\\s*github\\s*\\.\\s*event\\s*\\.\\s*(?:issue|pull_request)\\s*\\.\\s*labels\\b",
|
||||
_, _)
|
||||
) and
|
||||
not exists(
|
||||
this.getIf()
|
||||
.getValue()
|
||||
.regexpFind("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*label\\s*\\.\\s*name\\s*==", _, _)
|
||||
) and
|
||||
// actor check means only the user is able to run it
|
||||
not exists(this.getIf().getValue().regexpFind("\\bgithub\\s*\\.\\s*actor\\s*==", _, _))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action step that doesn't contain `actor` or `label` check in `if:` or
|
||||
*/
|
||||
class ProbablePullRequestTarget extends Actions::On, Actions::MappingOrSequenceOrScalar {
|
||||
ProbablePullRequestTarget() {
|
||||
exists(YAMLNode prtNode |
|
||||
// The `on:` is triggered on `pull_request_target`
|
||||
this.getNode("pull_request_target") = prtNode and
|
||||
(
|
||||
// and either doesn't contain `types` filter
|
||||
not exists(prtNode.getAChild())
|
||||
or
|
||||
// or has the filter, that is something else than just [labeled]
|
||||
exists(Actions::MappingOrSequenceOrScalar prt, Actions::MappingOrSequenceOrScalar types |
|
||||
types = prt.getNode("types") and
|
||||
prtNode = prt and
|
||||
(
|
||||
not types.getElementCount() = 1 or
|
||||
not exists(types.getNode("labeled"))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
from
|
||||
Actions::Ref ref, Actions::Uses uses, Actions::Step step, Actions::Job job,
|
||||
ProbablePullRequestTarget pullRequestTarget
|
||||
where
|
||||
pullRequestTarget.getWorkflow() = job.getWorkflow() and
|
||||
uses.getStep() = step and
|
||||
ref.getWith().getStep() = step and
|
||||
step.getJob() = job and
|
||||
uses.getGitHubRepository() = "actions/checkout" and
|
||||
(
|
||||
ref.getValue().matches("%github.event.pull_request.head.ref%") or
|
||||
ref.getValue().matches("%github.event.pull_request.head.sha%") or
|
||||
ref.getValue().matches("%github.event.pull_request.number%") or
|
||||
ref.getValue().matches("%github.event.number%") or
|
||||
ref.getValue().matches("%github.head_ref%")
|
||||
) and
|
||||
step instanceof ProbableStep and
|
||||
job instanceof ProbableJob
|
||||
select step, "Potential unsafe checkout of untrusted pull request on `pull_request_target`"
|
||||
@@ -0,0 +1,8 @@
|
||||
on: issue_comment
|
||||
|
||||
jobs:
|
||||
echo-body:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo '${{ github.event.comment.body }}'
|
||||
@@ -0,0 +1,10 @@
|
||||
on: issue_comment
|
||||
|
||||
jobs:
|
||||
echo-body:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
BODY: ${{ github.event.issue.body }}
|
||||
run: |
|
||||
echo '$BODY'
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Comment on the pull request
|
||||
|
||||
# read-write repo token
|
||||
# access to secrets
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Receive PR"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
${{ github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: 'Download artifact'
|
||||
uses: actions/github-script@v3.1.0
|
||||
with:
|
||||
script: |
|
||||
var artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{github.event.workflow_run.id }},
|
||||
});
|
||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "pr"
|
||||
})[0];
|
||||
var download = await github.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data));
|
||||
- run: unzip pr.zip
|
||||
|
||||
- name: 'Comment on PR'
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
var issue_number = Number(fs.readFileSync('./NR'));
|
||||
await github.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: 'Everything is OK. Thank you for the PR!'
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
on:
|
||||
pull_request_target
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
- run: |
|
||||
npm install
|
||||
npm build
|
||||
|
||||
- uses: completely/fakeaction@v2
|
||||
with:
|
||||
arg1: ${{ secrets.supersecret }}
|
||||
|
||||
- uses: fakerepo/comment-on-pr@v1
|
||||
with:
|
||||
message: |
|
||||
Thank you!
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Receive PR
|
||||
|
||||
# read-only repo token
|
||||
# no access to secrets
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# imitation of a build process
|
||||
- name: Build
|
||||
run: /bin/bash ./build.sh
|
||||
|
||||
- name: Save PR number
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo ${{ github.event.number }} > ./pr/NR
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: pr
|
||||
path: pr/
|
||||
316
javascript/ql/src/experimental/semmle/javascript/Actions.qll
Normal file
316
javascript/ql/src/experimental/semmle/javascript/Actions.qll
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Libraries for modelling GitHub Actions workflow files written in YAML.
|
||||
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Libraries for modelling 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
javascript/ql/test/experimental/Security/CWE-094/.github/workflows/comment_issue.yml
vendored
Normal file
8
javascript/ql/test/experimental/Security/CWE-094/.github/workflows/comment_issue.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
on: issue_comment
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo '${{ github.event.comment.body }}'
|
||||
@@ -0,0 +1,38 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
job1:
|
||||
if: contains(github.event.issue.labels.*.name, 'ok')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
job2:
|
||||
if: github.event.label.name == 'ok'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
job3:
|
||||
if: github.actor == 'ok'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
job4:
|
||||
if: github.actor == 'ok' || true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
job5:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1,31 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
if: contains(github.event.issue.labels.*.name, 'ok')
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
if: github.event.label.name == 'ok'
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
if: github.actor == 'ok'
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
if: github.actor == 'ok' || true
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1,12 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
push:
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1,13 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
labeled:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1,15 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
labeled:
|
||||
opened:
|
||||
closed:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1,12 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, opened]
|
||||
push:
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1,10 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1,10 @@
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
11
javascript/ql/test/experimental/Security/CWE-094/.github/workflows/pull_request_target_run.yml
vendored
Normal file
11
javascript/ql/test/experimental/Security/CWE-094/.github/workflows/pull_request_target_run.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
on: pull_request_target
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- run: make
|
||||
@@ -0,0 +1,9 @@
|
||||
on: [pull_request_target, push]
|
||||
|
||||
jobs:
|
||||
echo-chamber:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -0,0 +1 @@
|
||||
| .github/workflows/comment_issue.yml:7:12:8:47 | \| | Potential injection from the github.event.comment.body context, which may be controlled by an external user. |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-094/ExpressionInjection.ql
|
||||
@@ -0,0 +1,9 @@
|
||||
| .github/workflows/pull_request_target_if_job.yml:30:7:33:2 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_if_job.yml:36:7:38:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_if_step.yml:24:7:29:4 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_if_step.yml:29:7:31:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_labels_mapping.yml:13:7:15:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_labels_sequence.yml:10:7:12:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_mapping.yml:8:7:10:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_run.yml:7:7:11:4 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
| .github/workflows/pull_request_target_sequence.yml:7:7:9:54 | uses: a ... kout@v2 | Potential unsafe checkout of untrusted pull request on `pull_request_target` |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-094/UntrustedCheckout.ql
|
||||
@@ -0,0 +1 @@
|
||||
console.log('test')
|
||||
Reference in New Issue
Block a user