Skip to content

Commit 5860896

Browse files
noamd-legitnaveensrinivasan
authored andcommitted
detect workflow_run as a dangerous trigger
1 parent 606f28a commit 5860896

File tree

5 files changed

+92
-25
lines changed

5 files changed

+92
-25
lines changed

checks/dangerous_workflow.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ const (
8080
type triggerName string
8181

8282
var (
83-
triggerPullRequestTarget = triggerName("pull_request_target")
84-
triggerPullRequest = triggerName("pull_request")
85-
checkoutUntrustedRef = "github.event.pull_request"
83+
triggerPullRequestTarget = triggerName("pull_request_target")
84+
triggerWorkflowRun = triggerName("workflow_run")
85+
triggerPullRequest = triggerName("pull_request")
86+
checkoutUntrustedPullRequestRef = "github.event.pull_request"
87+
checkoutUntrustedWorkflowRunRef = "github.event.workflow_run"
8688
)
8789

8890
// Holds stateful data to pass thru callbacks.
@@ -168,7 +170,9 @@ func validateSecretsInPullRequests(workflow *actionlint.Workflow, path string,
168170
// We need pull request trigger.
169171
usesPullRequest := usesEventTrigger(workflow, triggerPullRequest)
170172
usesPullRequestTarget := usesEventTrigger(workflow, triggerPullRequestTarget)
171-
if !usesPullRequest && !usesPullRequestTarget {
173+
usesWorkflowRun := usesEventTrigger(workflow, triggerWorkflowRun)
174+
175+
if !usesPullRequest && !usesPullRequestTarget && !usesWorkflowRun {
172176
return nil
173177
}
174178

@@ -179,6 +183,9 @@ func validateSecretsInPullRequests(workflow *actionlint.Workflow, path string,
179183
if usesPullRequestTarget {
180184
triggers[triggerPullRequestTarget] = usesPullRequestTarget
181185
}
186+
if usesWorkflowRun {
187+
triggers[triggerWorkflowRun] = usesWorkflowRun
188+
}
182189

183190
// Secrets used in env at the top of the wokflow.
184191
if err := checkWorkflowSecretInEnv(workflow, triggers, path, dl, pdata); err != nil {
@@ -198,7 +205,7 @@ func validateSecretsInPullRequests(workflow *actionlint.Workflow, path string,
198205
func validateUntrustedCodeCheckout(workflow *actionlint.Workflow, path string,
199206
dl checker.DetailLogger, pdata *patternCbData,
200207
) error {
201-
if !usesEventTrigger(workflow, triggerPullRequestTarget) {
208+
if !usesEventTrigger(workflow, triggerPullRequestTarget) && !usesEventTrigger(workflow, triggerWorkflowRun) {
202209
return nil
203210
}
204211

@@ -240,16 +247,7 @@ func checkJobForUsedSecrets(job *actionlint.Job, triggers map[triggerName]bool,
240247

241248
// If the job has an environment, assume it's an env secret gated by
242249
// some approval and don't alert.
243-
if jobUsesEnvironment(job) {
244-
return nil
245-
}
246-
247-
// For pull request target, we need a ref to the pull request.
248-
_, usesPullRequest := triggers[triggerPullRequest]
249-
_, usesPullRequestTarget := triggers[triggerPullRequestTarget]
250-
chk, ref := jobUsesCodeCheckout(job)
251-
if !((chk && usesPullRequest) ||
252-
(chk && usesPullRequestTarget && strings.Contains(ref, checkoutUntrustedRef))) {
250+
if !jobUsesCodeCheckoutAndNoEnvironment(job, triggers) {
253251
return nil
254252
}
255253

@@ -281,17 +279,32 @@ func workflowUsesCodeCheckoutAndNoEnvironment(workflow *actionlint.Workflow,
281279
return false
282280
}
283281

282+
for _, job := range workflow.Jobs {
283+
if jobUsesCodeCheckoutAndNoEnvironment(job, triggers) {
284+
return true
285+
}
286+
}
287+
return false
288+
}
289+
290+
func jobUsesCodeCheckoutAndNoEnvironment(job *actionlint.Job, triggers map[triggerName]bool,
291+
) bool {
292+
if job == nil {
293+
return false
294+
}
284295
_, usesPullRequest := triggers[triggerPullRequest]
285296
_, usesPullRequestTarget := triggers[triggerPullRequestTarget]
297+
_, usesWorkflowRun := triggers[triggerWorkflowRun]
286298

287-
for _, job := range workflow.Jobs {
288-
chk, ref := jobUsesCodeCheckout(job)
289-
if ((chk && usesPullRequest) ||
290-
(chk && usesPullRequestTarget && strings.Contains(ref, checkoutUntrustedRef))) &&
291-
!jobUsesEnvironment(job) {
299+
chk, ref := jobUsesCodeCheckout(job)
300+
if !jobUsesEnvironment(job) {
301+
if (chk && usesPullRequest) ||
302+
(chk && usesPullRequestTarget && strings.Contains(ref, checkoutUntrustedPullRequestRef)) ||
303+
(chk && usesWorkflowRun && strings.Contains(ref, checkoutUntrustedWorkflowRunRef)) {
292304
return true
293305
}
294306
}
307+
295308
return false
296309
}
297310

@@ -348,7 +361,9 @@ func checkJobForUntrustedCodeCheckout(job *actionlint.Job, path string,
348361
if !ok || ref.Value == nil {
349362
continue
350363
}
351-
if strings.Contains(ref.Value.Value, checkoutUntrustedRef) {
364+
365+
if strings.Contains(ref.Value.Value, checkoutUntrustedPullRequestRef) ||
366+
strings.Contains(ref.Value.Value, checkoutUntrustedWorkflowRunRef) {
352367
line := fileparser.GetLineNumber(step.Pos)
353368
dl.Warn(&checker.LogMessage{
354369
Path: path,

checks/dangerous_workflow_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ func TestGithubDangerousWorkflow(t *testing.T) {
4343
NumberOfDebug: 0,
4444
},
4545
},
46+
{
47+
name: "run untrusted code checkout test - workflow_run",
48+
filename: "./testdata/.github/workflows/github-workflow-dangerous-pattern-untrusted-checkout-workflow_run.yml",
49+
expected: scut.TestReturn{
50+
Error: nil,
51+
Score: checker.MinResultScore,
52+
NumberOfWarn: 2,
53+
NumberOfInfo: 0,
54+
NumberOfDebug: 0,
55+
},
56+
},
4657
{
4758
name: "run untrusted code checkout test",
4859
filename: "./testdata/.github/workflows/github-workflow-dangerous-pattern-untrusted-checkout.yml",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2021 Security Scorecard Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
on:
15+
workflow_run:
16+
workflows: ['dummy']
17+
18+
jobs:
19+
build:
20+
name: Build and test
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v2
24+
with:
25+
ref: ${{ github.event.workflow_run }}
26+
27+
- uses: actions/setup-node@v1
28+
- run: |
29+
npm install
30+
npm build
31+
32+
- uses: completely/fakeaction@v2
33+
with:
34+
arg1: ${{ secrets.supersecret }}
35+
36+
- uses: fakerepo/comment-on-pr@v1
37+
with:
38+
message: |
39+
Thank you!

docs/checks.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,8 @@ logging github context and secrets, or use of potentially untrusted inputs in sc
264264
The following patterns are checked:
265265

266266
Untrusted Code Checkout: This is the misuse of potentially dangerous triggers.
267-
This checks if a `pull_request_target` workflow trigger was used in conjunction
268-
with an explicit pull request checkout. Workflows triggered with `pull_request_target`
267+
This checks if a `pull_request_target` or `workflow_run` workflow trigger was used in conjunction
268+
with an explicit pull request checkout. Workflows triggered with `pull_request_target` / `workflow_run`
269269
have write permission to the target repository and access to target repository
270270
secrets. With the PR checkout, PR authors may compromise the repository, for
271271
example, by using build scripts controlled by the author of the PR or reading
@@ -606,8 +606,10 @@ possible.
606606
Risk: `Critical` (service possibly accessible to third parties)
607607

608608
This check determines whether the webhook defined in the repository has a token configured to authenticate the origins of requests.
609+
609610

610611
**Remediation steps**
611612
- Check whether your service supports token authentication.
612613
- If there is support for token authentication, set the secret in the webhook configuration. See [Setting up a webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks#setting-up-a-webhook)
613614
- If there is no support for token authentication, consider implementing it by following [these directions](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).
615+

docs/checks/internal/checks.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,8 +674,8 @@ checks:
674674
The following patterns are checked:
675675
676676
Untrusted Code Checkout: This is the misuse of potentially dangerous triggers.
677-
This checks if a `pull_request_target` workflow trigger was used in conjunction
678-
with an explicit pull request checkout. Workflows triggered with `pull_request_target`
677+
This checks if a `pull_request_target` or `workflow_run` workflow trigger was used in conjunction
678+
with an explicit pull request checkout. Workflows triggered with `pull_request_target` / `workflow_run`
679679
have write permission to the target repository and access to target repository
680680
secrets. With the PR checkout, PR authors may compromise the repository, for
681681
example, by using build scripts controlled by the author of the PR or reading

0 commit comments

Comments
 (0)