Skip to content

Commit 2d6feb1

Browse files
committed
Fix false negatives when one of the jobs had proper checks and the other didn't
1 parent d51a9a3 commit 2d6feb1

7 files changed

Lines changed: 77 additions & 24 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
---
22
category: fix
33
---
4-
* GitHub Actions support for reusable workflows was improved.
4+
* GitHub Actions queries now better account for permission checks on jobs that call reusable workflows.

actions/ql/lib/codeql/actions/security/ControlChecks.qll

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ abstract class ControlCheck extends AstNode {
6262

6363
predicate protects(AstNode node, Event event, string category) {
6464
// The check dominates the step it should protect
65-
this.dominates(node) and
65+
this.dominates(node, event) and
6666
// The check is effective against the event and category
6767
this.protectsCategoryAndEvent(category, event.getName()) and
6868
// The check can be triggered by the event
@@ -72,7 +72,7 @@ abstract class ControlCheck extends AstNode {
7272
/**
7373
* Holds if this control check must execute and pass before `node` can run.
7474
*/
75-
predicate dominates(AstNode node) {
75+
predicate dominates(AstNode node, Event event) {
7676
// Step-level: the check is an `if:` on the step containing `node`,
7777
// or on the enclosing job, or on a needed job/step.
7878
this instanceof If and
@@ -104,26 +104,34 @@ abstract class ControlCheck extends AstNode {
104104
)
105105
or
106106
// When the node is inside a (possibly nested) reusable workflow,
107-
// check if the control check dominates any caller job in the chain.
108-
exists(ExternalJob directCaller, ExternalJob caller |
107+
// all direct callers for this event must be protected along their caller chain.
108+
exists(ExternalJob directCaller |
109109
directCaller = node.getEnclosingWorkflow().(ReusableWorkflow).getACaller() and
110-
caller = getAnOuterCaller*(directCaller) and
111-
(
112-
this instanceof If and
113-
(
114-
caller.getIf() = this or
115-
caller.getANeededJob().(LocalJob).getIf() = this or
116-
caller.getANeededJob().(LocalJob).getAStep().getIf() = this
117-
)
118-
or
119-
this instanceof Environment and
110+
directCaller.getATriggerEvent() = event
111+
) and
112+
forall(ExternalJob directCaller |
113+
directCaller = node.getEnclosingWorkflow().(ReusableWorkflow).getACaller() and
114+
directCaller.getATriggerEvent() = event
115+
|
116+
exists(ExternalJob caller |
117+
caller = getAnOuterCaller*(directCaller) and
120118
(
121-
caller.getEnvironment() = this or
122-
caller.getANeededJob().getEnvironment() = this
119+
this instanceof If and
120+
(
121+
caller.getIf() = this or
122+
caller.getANeededJob().(LocalJob).getIf() = this or
123+
caller.getANeededJob().(LocalJob).getAStep().getIf() = this
124+
)
125+
or
126+
this instanceof Environment and
127+
(
128+
caller.getEnvironment() = this or
129+
caller.getANeededJob().getEnvironment() = this
130+
)
131+
or
132+
(this instanceof Run or this instanceof UsesStep) and
133+
caller.getANeededJob().(LocalJob).getAStep() = this
123134
)
124-
or
125-
(this instanceof Run or this instanceof UsesStep) and
126-
caller.getANeededJob().(LocalJob).getAStep() = this
127135
)
128136
)
129137
}

actions/ql/src/Security/CWE-285/ImproperAccessControl.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ from LocalJob job, LabelCheck check, MutableRefCheckoutStep checkout, Event even
1818
where
1919
job.isPrivileged() and
2020
job.getAStep() = checkout and
21-
check.dominates(checkout) and
21+
check.dominates(checkout, event) and
2222
(
2323
job.getATriggerEvent() = event and
2424
event.getName() = "pull_request_target" and

actions/ql/src/Security/CWE-829/UntrustedCheckoutHigh.ql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ where
3434
check instanceof AssociationCheck or
3535
check instanceof PermissionCheck
3636
) and
37-
check.dominates(checkout) and
38-
date_check.dominates(checkout)
37+
check.dominates(checkout, event) and
38+
date_check.dominates(checkout, event)
3939
)
4040
or
4141
// not issue_comment triggered workflows
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
on:
2+
pull_request_target:
3+
4+
jobs:
5+
is-collaborator:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- name: Get User Permission
9+
id: checkAccess
10+
uses: actions-cool/check-user-permission@cd622002ff25c2311d2e7fb82107c0d24be83f9b
11+
with:
12+
require: write
13+
username: ${{ github.actor }}
14+
env:
15+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16+
- name: Check User Permission
17+
if: steps.checkAccess.outputs.require-result == 'false'
18+
run: |
19+
echo "${{ github.actor }} does not have permissions on this repo."
20+
echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}"
21+
exit 1
22+
build_unsafe:
23+
# needs: is-collaborator
24+
uses: TestOrg/TestRepo/.github/workflows/build.yml@main
25+
with:
26+
COMMIT_SHA: ${{ github.event.pull_request.head.sha }} # should alert since no permission check
27+
build_safe:
28+
needs: is-collaborator
29+
uses: TestOrg/TestRepo/.github/workflows/build.yml@main
30+
with:
31+
COMMIT_SHA: ${{ github.event.pull_request.head.sha }} # shouldn't alert since permission check

actions/ql/test/query-tests/Security/CWE-829/.github/workflows/untrusted_checkout_permissions_check.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ jobs:
2929
ref: ${{ github.event.pull_request.head.sha }} # shouldn't alert since permission check
3030
fetch-depth: 2
3131
- run: yarn test
32+
build_unsafe:
33+
runs-on: ubuntu-latest
34+
# needs: is-collaborator
35+
steps:
36+
- name: Checkout repo
37+
uses: actions/checkout@4
38+
with:
39+
ref: ${{ github.event.pull_request.head.sha }} # should alert since no permission check
40+
fetch-depth: 2
41+
- run: yarn test

actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,13 @@ edges
337337
| .github/workflows/untrusted_checkout_6.yml:17:9:21:6 | Uses Step | .github/workflows/untrusted_checkout_6.yml:21:9:23:23 | Run Step |
338338
| .github/workflows/untrusted_checkout_no_needs.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_no_needs.yml:16:9:22:2 | Run Step |
339339
| .github/workflows/untrusted_checkout_no_needs.yml:26:9:31:6 | Uses Step | .github/workflows/untrusted_checkout_no_needs.yml:31:9:31:23 | Run Step |
340+
| .github/workflows/untrusted_checkout_permission_check_reusable2.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permission_check_reusable2.yml:16:9:22:2 | Run Step |
340341
| .github/workflows/untrusted_checkout_permission_check_reusable.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permission_check_reusable.yml:16:9:22:2 | Run Step |
341342
| .github/workflows/untrusted_checkout_permission_check_reusable_level2.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permission_check_reusable_level2.yml:16:9:22:2 | Run Step |
342343
| .github/workflows/untrusted_checkout_permission_check_reusable_no_needs.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permission_check_reusable_no_needs.yml:16:9:22:2 | Run Step |
343344
| .github/workflows/untrusted_checkout_permissions_check.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permissions_check.yml:16:9:22:2 | Run Step |
344-
| .github/workflows/untrusted_checkout_permissions_check.yml:26:9:31:6 | Uses Step | .github/workflows/untrusted_checkout_permissions_check.yml:31:9:31:23 | Run Step |
345+
| .github/workflows/untrusted_checkout_permissions_check.yml:26:9:31:6 | Uses Step | .github/workflows/untrusted_checkout_permissions_check.yml:31:9:32:2 | Run Step |
346+
| .github/workflows/untrusted_checkout_permissions_check.yml:36:9:41:6 | Uses Step | .github/workflows/untrusted_checkout_permissions_check.yml:41:9:41:22 | Run Step |
345347
| .github/workflows/workflow_run_untrusted_checkout.yml:13:9:16:6 | Uses Step | .github/workflows/workflow_run_untrusted_checkout.yml:16:9:18:31 | Uses Step |
346348
| .github/workflows/workflow_run_untrusted_checkout_2.yml:13:9:16:6 | Uses Step | .github/workflows/workflow_run_untrusted_checkout_2.yml:16:9:18:31 | Uses Step |
347349
| .github/workflows/workflow_run_untrusted_checkout_3.yml:13:9:16:6 | Uses Step | .github/workflows/workflow_run_untrusted_checkout_3.yml:16:9:18:31 | Uses Step |
@@ -352,6 +354,7 @@ edges
352354
| .github/workflows/auto_ci.yml:67:9:74:6 | Uses Step | .github/workflows/auto_ci.yml:67:9:74:6 | Uses Step | .github/workflows/auto_ci.yml:79:9:84:6 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/auto_ci.yml:6:3:6:21 | pull_request_target | pull_request_target |
353355
| .github/workflows/auto_ci.yml:67:9:74:6 | Uses Step | .github/workflows/auto_ci.yml:67:9:74:6 | Uses Step | .github/workflows/auto_ci.yml:84:9:93:6 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/auto_ci.yml:6:3:6:21 | pull_request_target | pull_request_target |
354356
| .github/workflows/dependabot3.yml:15:9:20:6 | Uses Step | .github/workflows/dependabot3.yml:15:9:20:6 | Uses Step | .github/workflows/dependabot3.yml:25:9:48:6 | Run Step: set-milestone | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/dependabot3.yml:3:5:3:23 | pull_request_target | pull_request_target |
357+
| .github/workflows/external/TestOrg/TestRepo/.github/workflows/build.yml:11:9:14:6 | Uses Step | .github/workflows/external/TestOrg/TestRepo/.github/workflows/build.yml:11:9:14:6 | Uses Step | .github/workflows/external/TestOrg/TestRepo/.github/workflows/build.yml:14:9:17:7 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_permission_check_reusable2.yml:2:3:2:21 | pull_request_target | pull_request_target |
355358
| .github/workflows/external/TestOrg/TestRepo/.github/workflows/build.yml:11:9:14:6 | Uses Step | .github/workflows/external/TestOrg/TestRepo/.github/workflows/build.yml:11:9:14:6 | Uses Step | .github/workflows/external/TestOrg/TestRepo/.github/workflows/build.yml:14:9:17:7 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_permission_check_reusable_no_needs.yml:2:3:2:21 | pull_request_target | pull_request_target |
356359
| .github/workflows/external/TestOrg/TestRepo/.github/workflows/reusable.yml:23:9:26:6 | Uses Step | .github/workflows/external/TestOrg/TestRepo/.github/workflows/reusable.yml:23:9:26:6 | Uses Step | .github/workflows/external/TestOrg/TestRepo/.github/workflows/reusable.yml:26:9:29:7 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/reusable_caller1.yaml:4:3:4:21 | pull_request_target | pull_request_target |
357360
| .github/workflows/gitcheckout.yml:10:11:18:8 | Run Step | .github/workflows/gitcheckout.yml:10:11:18:8 | Run Step | .github/workflows/gitcheckout.yml:21:11:23:22 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/gitcheckout.yml:2:3:2:21 | pull_request_target | pull_request_target |
@@ -387,3 +390,4 @@ edges
387390
| .github/workflows/untrusted_checkout.yml:8:9:11:6 | Uses Step | .github/workflows/untrusted_checkout.yml:8:9:11:6 | Uses Step | .github/workflows/untrusted_checkout.yml:15:9:18:2 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout.yml:2:3:2:21 | pull_request_target | pull_request_target |
388391
| .github/workflows/untrusted_checkout.yml:23:9:26:6 | Uses Step | .github/workflows/untrusted_checkout.yml:23:9:26:6 | Uses Step | .github/workflows/untrusted_checkout.yml:30:9:32:23 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout.yml:2:3:2:21 | pull_request_target | pull_request_target |
389392
| .github/workflows/untrusted_checkout_no_needs.yml:26:9:31:6 | Uses Step | .github/workflows/untrusted_checkout_no_needs.yml:26:9:31:6 | Uses Step | .github/workflows/untrusted_checkout_no_needs.yml:31:9:31:23 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_no_needs.yml:2:3:2:21 | pull_request_target | pull_request_target |
393+
| .github/workflows/untrusted_checkout_permissions_check.yml:36:9:41:6 | Uses Step | .github/workflows/untrusted_checkout_permissions_check.yml:36:9:41:6 | Uses Step | .github/workflows/untrusted_checkout_permissions_check.yml:41:9:41:22 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_permissions_check.yml:2:3:2:21 | pull_request_target | pull_request_target |

0 commit comments

Comments
 (0)