Skip to content

Commit 1bf2431

Browse files
author
Alvaro Muñoz
committed
Improve UntrustedCheckout query
Account for more events, more triggers and heuristics to detect git checkouts
1 parent aa62603 commit 1bf2431

File tree

2 files changed

+80
-28
lines changed

2 files changed

+80
-28
lines changed

ql/src/Security/CWE-829/UntrustedCheckout.ql

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,68 @@
1515

1616
import actions
1717

18-
/**
19-
* An If node that contains an `actor` check
20-
*/
21-
class ActorCheck extends If {
22-
ActorCheck() {
18+
/** An If node that contains an actor, user or label check */
19+
class ControlCheck extends If {
20+
ControlCheck() {
2321
this.getCondition().regexpMatch(".*github\\.(triggering_)?actor.*") or
24-
this.getCondition().regexpMatch(".*github\\.event\\.pull_request\\.user\\.login.*")
22+
this.getCondition().regexpMatch(".*github\\.event\\.pull_request\\.user\\.login.*") or
23+
this.getCondition().regexpMatch(".*github\\.event\\.pull_request\\.labels.*") or
24+
this.getCondition().regexpMatch(".*github\\.event\\.label\\.name.*")
2525
}
2626
}
2727

28-
/**
29-
* An If node that contains a `label` check
30-
*/
31-
class LabelCheck extends If {
32-
LabelCheck() {
33-
this.getCondition().regexpMatch(".*github\\.event\\.pull_request\\.labels.*") or
34-
this.getCondition().regexpMatch(".*github\\.event\\.label\\.name.*")
28+
bindingset[s]
29+
predicate containsHeadRef(string s) {
30+
s.matches("%" +
31+
[
32+
"github.event.number", // The pull request number.
33+
"github.event.pull_request.head.ref", // The ref name of head.
34+
"github.event.pull_request.head.sha", // The commit SHA of head.
35+
"github.event.pull_request.id", // The pull request ID.
36+
"github.event.pull_request.number", // The pull request number.
37+
"github.event.pull_request.merge_commit_sha", // The SHA of the merge commit.
38+
"github.head_ref", // The head_ref or source branch of the pull request in a workflow run.
39+
"github.event.workflow_run.head_branch", // The branch of the head commit.
40+
"github.event.workflow_run.head_commit.id", // The SHA of the head commit.
41+
"github.event.workflow_run.head_sha", // The SHA of the head commit.
42+
"env.GITHUB_HEAD_REF",
43+
] + "%")
44+
}
45+
46+
/** Checkout of a Pull Request HEAD ref */
47+
abstract class PRHeadCheckoutStep extends Step { }
48+
49+
/** Checkout of a Pull Request HEAD ref using actions/checkout action */
50+
class ActionsCheckout extends PRHeadCheckoutStep instanceof UsesStep {
51+
ActionsCheckout() {
52+
this.getCallee() = "actions/checkout" and
53+
containsHeadRef(this.getArgumentExpr("ref").getExpression())
54+
}
55+
}
56+
57+
/** Checkout of a Pull Request HEAD ref using git within a Run step */
58+
class GitCheckout extends PRHeadCheckoutStep instanceof Run {
59+
GitCheckout() {
60+
exists(string line |
61+
this.getScript().splitAt("\n") = line and
62+
line.regexpMatch(".*git\\s+fetch.*") and
63+
(
64+
containsHeadRef(line)
65+
or
66+
exists(string varname |
67+
containsHeadRef(this.getInScopeEnvVarExpr(varname).getExpression()) and
68+
line.matches("%" + varname + "%")
69+
)
70+
)
71+
)
3572
}
3673
}
3774

38-
from Workflow w, LocalJob job, UsesStep checkoutStep
75+
from Workflow w, PRHeadCheckoutStep checkout
3976
where
40-
w.hasTriggerEvent("pull_request_target") and
41-
w.getAJob() = job and
42-
job.getAStep() = checkoutStep and
43-
checkoutStep.getCallee() = "actions/checkout" and
44-
checkoutStep
45-
.getArgumentExpr("ref")
46-
.getExpression()
47-
.matches([
48-
"%github.event.pull_request.head.ref%", "%github.event.pull_request.head.sha%",
49-
"%github.event.pull_request.number%", "%github.event.number%", "%github.head_ref%"
50-
]) and
51-
not exists(ActorCheck check | job.getIf() = check or checkoutStep.getIf() = check) and
52-
not exists(LabelCheck check | job.getIf() = check or checkoutStep.getIf() = check)
53-
select checkoutStep, "Potential unsafe checkout of untrusted pull request on 'pull_request_target'."
77+
w.hasTriggerEvent(["pull_request_target", "issue_comment", "workflow_run"]) and
78+
w.getAJob().(LocalJob).getAStep() = checkout and
79+
not exists(ControlCheck check |
80+
checkout.getIf() = check or checkout.getEnclosingJob().getIf() = check
81+
)
82+
select checkout, "Potential unsafe checkout of untrusted pull request on privileged workflow."
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
on:
2+
pull_request_target
3+
4+
jobs:
5+
build:
6+
name: Build and test
7+
runs-on: ubuntu-latest
8+
steps:
9+
# 1. Check out the content from an incoming pull request
10+
- run: |
11+
git fetch origin $HEAD_BRANCH
12+
git checkout origin/master
13+
git config user.name "release-hash-check"
14+
git config user.email "<>"
15+
git merge --no-commit --no-edit origin/$HEAD_BRANCH
16+
env:
17+
HEAD_BRANCH: ${{ github.head_ref }}
18+
- uses: actions/setup-node@v1
19+
# 2. Potentially untrusted commands are being run during "npm install" or "npm build" as
20+
# the build scripts and referenced packages are controlled by the author of the pull request
21+
- run: |
22+
npm install
23+
npm build

0 commit comments

Comments
 (0)