Skip to content

Commit d85deef

Browse files
committed
Detect unpinned reusable workflows
1 parent bfc7a8b commit d85deef

5 files changed

Lines changed: 97 additions & 14 deletions

File tree

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
## Overview
22

3-
Using a tag for a 3rd party Action that is not pinned to a commit can lead to executing an untrusted Action through a supply chain attack.
3+
Using a mutable tag or branch for an Action or reusable workflow can lead to executing untrusted code through a supply chain attack.
44

55
## Recommendation
66

7-
Pinning an action to a full length commit SHA is currently the only way to use a non-immutable action as an immutable release. Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository, as they would need to generate a SHA-1 collision for a valid Git object payload. When selecting a SHA, you should verify it is from the action's repository and not a repository fork.
7+
Pinning an Action or reusable workflow to a full length commit SHA is currently the only way to use a mutable reference as an immutable release. Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the referenced repository, as they would need to generate a SHA-1 collision for a valid Git object payload. When selecting a SHA, you should verify it is from the intended repository and not a repository fork.
88

99
## Example
1010

@@ -14,12 +14,26 @@ Pinning an action to a full length commit SHA is currently the only way to use a
1414
- uses: tj-actions/changed-files@v44
1515
```
1616
17+
```yaml
18+
jobs:
19+
call-workflow:
20+
uses: example/actions/.github/workflows/build.yml@main
21+
```
22+
1723
### Correct Usage
1824
1925
```yaml
2026
- uses: tj-actions/changed-files@c65cd883420fd2eb864698a825fc4162dd94482c # v44
2127
```
2228
29+
```yaml
30+
jobs:
31+
call-workflow:
32+
uses: example/actions/.github/workflows/build.yml@25b062c917b0c75f8b47d8469aff6c94ffd89abb
33+
```
34+
2335
## References
2436
2537
- GitHub Docs: [Using third-party actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions).
38+
- GitHub Docs: [Reusing third-party workflows](https://docs.github.com/en/actions/reference/security/secure-use#reusing-third-party-workflows).
39+
- GitHub Docs: [Workflow syntax for reusable workflow calls](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_iduses).

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

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* @name Unpinned tag for a non-immutable Action in workflow or composite action
3-
* @description Using a tag for a non-immutable Action that is not pinned to a commit can lead to executing an untrusted Action through a supply chain attack.
2+
* @name Unpinned tag for a non-immutable Action or reusable workflow
3+
* @description Using a mutable reference for a non-immutable Action or reusable workflow can lead to executing untrusted code through a supply chain attack.
44
* @kind problem
55
* @security-severity 5.0
66
* @problem.severity warning
@@ -33,7 +33,7 @@ private predicate isPinnedContainer(string version) {
3333
bindingset[nwo]
3434
private predicate isContainerImage(string nwo) { nwo.regexpMatch("^docker://.+") }
3535

36-
private predicate getStepContainerName(UsesStep uses, string name) {
36+
private predicate hasUsesContainerName(Uses uses, string name) {
3737
exists(Workflow workflow |
3838
uses.getEnclosingWorkflow() = workflow and
3939
(
@@ -43,20 +43,32 @@ private predicate getStepContainerName(UsesStep uses, string name) {
4343
)
4444
)
4545
or
46-
exists(CompositeAction action |
47-
uses.getEnclosingCompositeAction() = action and
46+
exists(UsesStep step, CompositeAction action |
47+
uses = step and
48+
step.getEnclosingCompositeAction() = action and
4849
name = action.getLocation().getFile().getBaseName()
4950
)
5051
}
5152

52-
from UsesStep uses, string nwo, string version, string name
53+
from Uses uses, string nwo, string version, string name, string message
5354
where
5455
uses.getCallee() = nwo and
55-
getStepContainerName(uses, name) and
56+
hasUsesContainerName(uses, name) and
5657
uses.getVersion() = version and
5758
not isTrustedOwner(nwo) and
58-
not (if isContainerImage(nwo) then isPinnedContainer(version) else isPinnedCommit(version)) and
59-
not isImmutableAction(uses, nwo)
60-
select uses.getCalleeNode(),
61-
"Unpinned 3rd party Action '" + name + "' step $@ uses '" + nwo + "' with ref '" + version +
62-
"', not a pinned commit hash", uses, uses.toString()
59+
not (
60+
if uses instanceof UsesStep and isContainerImage(nwo)
61+
then isPinnedContainer(version)
62+
else isPinnedCommit(version)
63+
) and
64+
not exists(UsesStep step | uses = step and isImmutableAction(step, nwo)) and
65+
if uses instanceof ExternalJob
66+
then
67+
message =
68+
"Job $@ in '" + name + "' uses reusable workflow '" + nwo + "' with ref '" + version +
69+
"', not a pinned commit hash"
70+
else
71+
message =
72+
"Unpinned 3rd party Action '" + name + "' step $@ uses '" + nwo + "' with ref '" + version +
73+
"', not a pinned commit hash"
74+
select uses.getCalleeNode(), message, uses, uses.toString()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The `actions/unpinned-tag` query now detects mutable references to reusable workflows.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Reusable workflow pinning
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
unpinned-tag:
8+
uses: example/actions/.github/workflows/build.yml@v2
9+
10+
unpinned-branch:
11+
uses: example/actions/.github/workflows/build.yml@main
12+
13+
unpinned-branch-with-slash:
14+
uses: example/actions/.github/workflows/build.yml@release/v2
15+
16+
unpinned-short-sha:
17+
uses: example/actions/.github/workflows/build.yml@25b062c917b0c75f8b47d8469aff6c94ffd89ab
18+
19+
pinned-sha1:
20+
uses: example/actions/.github/workflows/build.yml@25b062c917b0c75f8b47d8469aff6c94ffd89abb
21+
22+
pinned-sha256:
23+
uses: example/actions/.github/workflows/build.yml@25b062c917b0c75f8b47d8469aff6c94ffd89abb25b062c917b0c75f8b47d84d
24+
25+
pinned-uppercase-sha1:
26+
uses: example/actions/.github/workflows/build.yml@25B062C917B0C75F8B47D8469AFF6C94FFD89ABB
27+
28+
trusted-owner:
29+
uses: actions/reusable-workflows/.github/workflows/build.yml@main
30+
31+
local-workflow:
32+
uses: ./.github/workflows/reusable_local.yml
33+
34+
local-workflow-dollar:
35+
uses: $/.github/workflows/reusable_local.yml

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
| .github/workflows/auto_ci.yml:94:15:94:39 | codecov/codecov-action@v3 | Unpinned 3rd party Action 'Python CI' step $@ uses 'codecov/codecov-action' with ref 'v3', not a pinned commit hash | .github/workflows/auto_ci.yml:93:9:96:6 | Uses Step | Uses Step |
99
| .github/workflows/auto_ci.yml:111:15:111:48 | peter-evans/create-pull-request@v5 | Unpinned 3rd party Action 'Python CI' step $@ uses 'peter-evans/create-pull-request' with ref 'v5', not a pinned commit hash | .github/workflows/auto_ci.yml:108:9:119:6 | Uses Step: create_pr | Uses Step: create_pr |
1010
| .github/workflows/auto_ci.yml:127:15:127:56 | thollander/actions-comment-pull-request@v2 | Unpinned 3rd party Action 'Python CI' step $@ uses 'thollander/actions-comment-pull-request' with ref 'v2', not a pinned commit hash | .github/workflows/auto_ci.yml:125:9:133:6 | Uses Step | Uses Step |
11+
| .github/workflows/external/TestOrg/TestRepo/.github/workflows/build_nested.yml:9:11:9:59 | TestOrg/TestRepo/.github/workflows/build.yml@main | Job $@ in 'build_nested.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build.yml' with ref 'main', not a pinned commit hash | .github/workflows/external/TestOrg/TestRepo/.github/workflows/build_nested.yml:9:5:13:7 | Job: build | Job: build |
12+
| .github/workflows/external/TestOrg/TestRepo/.github/workflows/build_nested_branching.yml:27:11:27:66 | TestOrg/TestRepo/.github/workflows/build_nested.yml@main | Job $@ in 'build_nested_branching.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build_nested.yml' with ref 'main', not a pinned commit hash | .github/workflows/external/TestOrg/TestRepo/.github/workflows/build_nested_branching.yml:26:5:30:2 | Job: build_safe | Job: build_safe |
13+
| .github/workflows/external/TestOrg/TestRepo/.github/workflows/build_nested_branching.yml:31:11:31:66 | TestOrg/TestRepo/.github/workflows/build_nested.yml@main | Job $@ in 'build_nested_branching.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build_nested.yml' with ref 'main', not a pinned commit hash | .github/workflows/external/TestOrg/TestRepo/.github/workflows/build_nested_branching.yml:31:5:33:43 | Job: build_unsafe | Job: build_unsafe |
14+
| .github/workflows/formal.yml:12:11:12:60 | TestOrg/TestRepo/.github/workflows/formal.yml@main | Job $@ in 'Test Formalities' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/formal.yml' with ref 'main', not a pinned commit hash | .github/workflows/formal.yml:11:5:12:61 | Job: build | Job: build |
1115
| .github/workflows/issue_comment_3rd_party_action.yml:14:15:14:52 | xt0rted/pull-request-comment-branch@v2 | Unpinned 3rd party Action 'PR head from 3rd party action' step $@ uses 'xt0rted/pull-request-comment-branch' with ref 'v2', not a pinned commit hash | .github/workflows/issue_comment_3rd_party_action.yml:12:9:16:6 | Uses Step: comment-branch | Uses Step: comment-branch |
1216
| .github/workflows/issue_comment_3rd_party_action.yml:27:15:27:52 | xt0rted/pull-request-comment-branch@v2 | Unpinned 3rd party Action 'PR head from 3rd party action' step $@ uses 'xt0rted/pull-request-comment-branch' with ref 'v2', not a pinned commit hash | .github/workflows/issue_comment_3rd_party_action.yml:25:9:30:6 | Uses Step: comment-branch | Uses Step: comment-branch |
1317
| .github/workflows/issue_comment_3rd_party_action.yml:41:15:41:42 | eficode/resolve-pr-refs@main | Unpinned 3rd party Action 'PR head from 3rd party action' step $@ uses 'eficode/resolve-pr-refs' with ref 'main', not a pinned commit hash | .github/workflows/issue_comment_3rd_party_action.yml:39:9:45:6 | Uses Step: refs | Uses Step: refs |
@@ -28,10 +32,24 @@
2832
| .github/workflows/pr-workflow.yml:449:15:449:43 | cachix/install-nix-action@v20 | Unpinned 3rd party Action 'pr-workflow' step $@ uses 'cachix/install-nix-action' with ref 'v20', not a pinned commit hash | .github/workflows/pr-workflow.yml:449:9:452:6 | Uses Step | Uses Step |
2933
| .github/workflows/pr-workflow.yml:452:15:452:60 | DeterminateSystems/magic-nix-cache-action@main | Unpinned 3rd party Action 'pr-workflow' step $@ uses 'DeterminateSystems/magic-nix-cache-action' with ref 'main', not a pinned commit hash | .github/workflows/pr-workflow.yml:452:9:453:6 | Uses Step | Uses Step |
3034
| .github/workflows/pr-workflow.yml:453:15:453:41 | cachix/cachix-action@master | Unpinned 3rd party Action 'pr-workflow' step $@ uses 'cachix/cachix-action' with ref 'master', not a pinned commit hash | .github/workflows/pr-workflow.yml:453:9:459:6 | Uses Step | Uses Step |
35+
| .github/workflows/reusable_caller1.yaml:8:11:8:62 | TestOrg/TestRepo/.github/workflows/reusable.yml@main | Job $@ in 'assets-test' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/reusable.yml' with ref 'main', not a pinned commit hash | .github/workflows/reusable_caller1.yaml:8:5:10:56 | Job: check-execution-context | Job: check-execution-context |
36+
| .github/workflows/reusable_caller2.yaml:8:11:8:62 | TestOrg/TestRepo/.github/workflows/reusable.yml@main | Job $@ in 'assets-test' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/reusable.yml' with ref 'main', not a pinned commit hash | .github/workflows/reusable_caller2.yaml:8:5:10:56 | Job: check-execution-context | Job: check-execution-context |
3137
| .github/workflows/test7.yml:25:15:25:34 | pnpm/action-setup@v3 | Unpinned 3rd party Action 'Benchmark' step $@ uses 'pnpm/action-setup' with ref 'v3', not a pinned commit hash | .github/workflows/test7.yml:24:9:27:6 | Uses Step | Uses Step |
3238
| .github/workflows/test13.yml:15:13:15:53 | sushichop/action-repository-permission@v2 | Unpinned 3rd party Action 'test13.yml' step $@ uses 'sushichop/action-repository-permission' with ref 'v2', not a pinned commit hash | .github/workflows/test13.yml:14:7:20:4 | Uses Step | Uses Step |
3339
| .github/workflows/test17.yml:20:21:20:63 | sonarsource/sonarcloud-github-action@master | Unpinned 3rd party Action 'Sonar' step $@ uses 'sonarsource/sonarcloud-github-action' with ref 'master', not a pinned commit hash | .github/workflows/test17.yml:19:15:23:58 | Uses Step | Uses Step |
3440
| .github/workflows/test18.yml:37:21:37:63 | sonarsource/sonarcloud-github-action@master | Unpinned 3rd party Action 'Sonar' step $@ uses 'sonarsource/sonarcloud-github-action' with ref 'master', not a pinned commit hash | .github/workflows/test18.yml:36:15:40:58 | Uses Step | Uses Step |
41+
| .github/workflows/unpinned_reusable_workflows.yml:8:11:8:56 | example/actions/.github/workflows/build.yml@v2 | Job $@ in 'Reusable workflow pinning' uses reusable workflow 'example/actions/.github/workflows/build.yml' with ref 'v2', not a pinned commit hash | .github/workflows/unpinned_reusable_workflows.yml:8:5:10:2 | Job: unpinned-tag | Job: unpinned-tag |
42+
| .github/workflows/unpinned_reusable_workflows.yml:11:11:11:58 | example/actions/.github/workflows/build.yml@main | Job $@ in 'Reusable workflow pinning' uses reusable workflow 'example/actions/.github/workflows/build.yml' with ref 'main', not a pinned commit hash | .github/workflows/unpinned_reusable_workflows.yml:11:5:13:2 | Job: unpinned-branch | Job: unpinned-branch |
43+
| .github/workflows/unpinned_reusable_workflows.yml:14:11:14:64 | example/actions/.github/workflows/build.yml@release/v2 | Job $@ in 'Reusable workflow pinning' uses reusable workflow 'example/actions/.github/workflows/build.yml' with ref 'release/v2', not a pinned commit hash | .github/workflows/unpinned_reusable_workflows.yml:14:5:16:2 | Job: unpinned-branch-with-slash | Job: unpinned-branch-with-slash |
44+
| .github/workflows/unpinned_reusable_workflows.yml:17:11:17:93 | example/actions/.github/workflows/build.yml@25b062c917b0c75f8b47d8469aff6c94ffd89ab | Job $@ in 'Reusable workflow pinning' uses reusable workflow 'example/actions/.github/workflows/build.yml' with ref '25b062c917b0c75f8b47d8469aff6c94ffd89ab', not a pinned commit hash | .github/workflows/unpinned_reusable_workflows.yml:17:5:19:2 | Job: unpinned-short-sha | Job: unpinned-short-sha |
3545
| .github/workflows/unpinned_tags.yml:10:13:10:22 | foo/bar@v1 | Unpinned 3rd party Action 'unpinned_tags.yml' step $@ uses 'foo/bar' with ref 'v1', not a pinned commit hash | .github/workflows/unpinned_tags.yml:10:7:11:4 | Uses Step | Uses Step |
3646
| .github/workflows/unpinned_tags.yml:12:13:12:35 | docker://foo/bar@latest | Unpinned 3rd party Action 'unpinned_tags.yml' step $@ uses 'docker://foo/bar' with ref 'latest', not a pinned commit hash | .github/workflows/unpinned_tags.yml:12:7:13:4 | Uses Step | Uses Step |
3747
| .github/workflows/unpinned_tags.yml:19:13:19:70 | foo/bar@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2a1b2c3d4e5 | Unpinned 3rd party Action 'unpinned_tags.yml' step $@ uses 'foo/bar' with ref 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2a1b2c3d4e5', not a pinned commit hash | .github/workflows/unpinned_tags.yml:19:7:19:71 | Uses Step | Uses Step |
48+
| .github/workflows/untrusted_checkout_permission_check_reusable2.yml:24:11:24:59 | TestOrg/TestRepo/.github/workflows/build.yml@main | Job $@ in 'untrusted_checkout_permission_check_reusable2.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_permission_check_reusable2.yml:24:5:27:2 | Job: build_unsafe | Job: build_unsafe |
49+
| .github/workflows/untrusted_checkout_permission_check_reusable2.yml:29:11:29:59 | TestOrg/TestRepo/.github/workflows/build.yml@main | Job $@ in 'untrusted_checkout_permission_check_reusable2.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_permission_check_reusable2.yml:28:5:31:101 | Job: build_safe | Job: build_safe |
50+
| .github/workflows/untrusted_checkout_permission_check_reusable.yml:24:11:24:59 | TestOrg/TestRepo/.github/workflows/build.yml@main | Job $@ in 'untrusted_checkout_permission_check_reusable.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_permission_check_reusable.yml:23:5:26:101 | Job: build | Job: build |
51+
| .github/workflows/untrusted_checkout_permission_check_reusable_branching_nested.yml:6:11:6:76 | TestOrg/TestRepo/.github/workflows/build_nested_branching.yml@main | Job $@ in 'untrusted_checkout_permission_check_reusable_branching_nested.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build_nested_branching.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_permission_check_reusable_branching_nested.yml:6:5:8:60 | Job: build | Job: build |
52+
| .github/workflows/untrusted_checkout_permission_check_reusable_level2.yml:24:11:24:66 | TestOrg/TestRepo/.github/workflows/build_nested.yml@main | Job $@ in 'untrusted_checkout_permission_check_reusable_level2.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build_nested.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_permission_check_reusable_level2.yml:23:5:26:101 | Job: build | Job: build |
53+
| .github/workflows/untrusted_checkout_permission_check_reusable_no_needs.yml:24:11:24:66 | TestOrg/TestRepo/.github/workflows/build_nested.yml@main | Job $@ in 'untrusted_checkout_permission_check_reusable_no_needs.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build_nested.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_permission_check_reusable_no_needs.yml:24:5:26:60 | Job: build | Job: build |
54+
| .github/workflows/untrusted_checkout_two_callers_both_protected.yml:24:11:24:59 | TestOrg/TestRepo/.github/workflows/build.yml@main | Job $@ in 'untrusted_checkout_two_callers_both_protected.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_two_callers_both_protected.yml:23:5:27:2 | Job: caller-a | Job: caller-a |
55+
| .github/workflows/untrusted_checkout_two_callers_both_protected.yml:46:11:46:59 | TestOrg/TestRepo/.github/workflows/build.yml@main | Job $@ in 'untrusted_checkout_two_callers_both_protected.yml' uses reusable workflow 'TestOrg/TestRepo/.github/workflows/build.yml' with ref 'main', not a pinned commit hash | .github/workflows/untrusted_checkout_two_callers_both_protected.yml:45:5:48:60 | Job: caller-b | Job: caller-b |

0 commit comments

Comments
 (0)