Skip to content

Commit f5355e7

Browse files
author
Alvaro Muñoz
authored
Merge pull request #20 from GitHubSecurityLab/untrusted_checkout
2 parents d0b904a + 68f15f2 commit f5355e7

File tree

5 files changed

+146
-3
lines changed

5 files changed

+146
-3
lines changed

ql/lib/codeql/actions/Ast.qll

+21-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ class AstNode instanceof YamlNode {
2222
*/
2323
class Statement extends AstNode {
2424
/** Gets the workflow that this job is a part of. */
25-
WorkflowStmt getEnclosingWorkflowStmt() {
26-
this = result.getAChildNode*()
27-
}
25+
WorkflowStmt getEnclosingWorkflowStmt() { this = result.getAChildNode*() }
2826
}
2927

3028
/**
@@ -174,6 +172,8 @@ class JobStmt extends Statement instanceof Actions::Job {
174172
predicate usesReusableWorkflow() {
175173
this.(YamlMapping).maps(any(YamlString s | s.getValue() = "uses"), _)
176174
}
175+
176+
IfStmt getIfStmt() { result = super.getIf() }
177177
}
178178

179179
/**
@@ -183,6 +183,24 @@ class StepStmt extends Statement instanceof Actions::Step {
183183
string getId() { result = super.getId() }
184184

185185
JobStmt getJobStmt() { result = super.getJob() }
186+
187+
IfStmt getIfStmt() { result = super.getIf() }
188+
}
189+
190+
/**
191+
* An If node representing a conditional statement.
192+
*/
193+
class IfStmt extends Statement {
194+
YamlMapping parent;
195+
196+
IfStmt() {
197+
(parent instanceof Actions::Step or parent instanceof Actions::Job) and
198+
parent.lookup("if") = this
199+
}
200+
201+
Statement getEnclosingStatement() { result = parent }
202+
203+
string getCondition() { result = this.(YamlScalar).getValue() }
186204
}
187205

188206
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @name Checkout of untrusted code in trusted context
3+
* @description Workflows triggered on `pull_request_target` have read/write access to the base repository and access to secrets.
4+
* By explicitly checking out and running the build script from a fork the untrusted code is running in an environment
5+
* that is able to push to the base repository and to access secrets.
6+
* @kind problem
7+
* @problem.severity warning
8+
* @precision low
9+
* @id actions/untrusted-checkout
10+
* @tags actions
11+
* security
12+
* external/cwe/cwe-094
13+
*/
14+
15+
import actions
16+
17+
/**
18+
* An If node that contains an `actor` check
19+
*/
20+
class ActorCheckStmt extends IfStmt {
21+
ActorCheckStmt() { this.getCondition().regexpMatch(".*github\\.(triggering_)?actor.*") }
22+
}
23+
24+
/**
25+
* An If node that contains a `label` check
26+
*/
27+
class LabelCheckStmt extends IfStmt {
28+
LabelCheckStmt() { this.getCondition().regexpMatch(".*github\\.event\\.pull_request\\.labels.*") }
29+
}
30+
31+
from WorkflowStmt w, JobStmt job, StepUsesExpr checkoutStep
32+
where
33+
w.hasTriggerEvent("pull_request_target") and
34+
w.getAJobStmt() = job and
35+
job.getAStepStmt() = checkoutStep and
36+
checkoutStep.getCallee() = "actions/checkout" and
37+
checkoutStep
38+
.getArgumentExpr("ref")
39+
.(ExprAccessExpr)
40+
.getExpression()
41+
.matches([
42+
"%github.event.pull_request.head.ref%", "%github.event.pull_request.head.sha%",
43+
"%github.event.pull_request.number%", "%github.event.number%", "%github.head_ref%"
44+
]) and
45+
not exists(ActorCheckStmt check | job.getIfStmt() = check or checkoutStep.getIfStmt() = check) and
46+
not exists(LabelCheckStmt check | job.getIfStmt() = check or checkoutStep.getIfStmt() = check)
47+
select checkoutStep, "Potential unsafe checkout of untrusted pull request on 'pull_request_target'."
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
on:
2+
pull_request_target
3+
4+
jobs:
5+
build:
6+
name: Build and test
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v2
10+
if: ${{ github.actor == "admin" }}
11+
with:
12+
ref: ${{ github.event.pull_request.head.sha }}
13+
14+
- uses: actions/setup-node@v1
15+
- run: |
16+
npm install
17+
npm build
18+
19+
- uses: completely/fakeaction@v2
20+
with:
21+
arg1: ${{ secrets.supersecret }}
22+
23+
- uses: fakerepo/comment-on-pr@v1
24+
with:
25+
message: |
26+
Thank you!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
on:
2+
pull_request_target:
3+
types: [labeled]
4+
5+
jobs:
6+
build:
7+
name: Build and test
8+
runs-on: ubuntu-latest
9+
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
10+
steps:
11+
- uses: actions/checkout@v2
12+
with:
13+
ref: ${{ github.event.pull_request.head.sha }}
14+
15+
- uses: actions/setup-node@v1
16+
- run: |
17+
npm install
18+
npm build
19+
20+
- uses: completely/fakeaction@v2
21+
with:
22+
arg1: ${{ secrets.supersecret }}
23+
24+
- uses: fakerepo/comment-on-pr@v1
25+
with:
26+
message: |
27+
Thank you!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
on:
2+
pull_request_target
3+
4+
jobs:
5+
build:
6+
name: Build and test
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v2
10+
with:
11+
ref: ${{ github.event.pull_request.head.sha }}
12+
13+
- uses: actions/setup-node@v1
14+
- run: |
15+
npm install
16+
npm build
17+
18+
- uses: completely/fakeaction@v2
19+
with:
20+
arg1: ${{ secrets.supersecret }}
21+
22+
- uses: fakerepo/comment-on-pr@v1
23+
with:
24+
message: |
25+
Thank you!

0 commit comments

Comments
 (0)