Skip to content

Commit cf9b853

Browse files
committed
unversioned immutable actions wip
1 parent 325727e commit cf9b853

5 files changed

Lines changed: 105 additions & 0 deletions

File tree

ql/lib/codeql/actions/config/Config.qll

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ predicate vulnerableActionsDataModel(
119119
Extensions::vulnerableActionsDataModel(action, vulnerable_version, vulnerable_sha, fixed_version)
120120
}
121121

122+
/**
123+
* MaD models for vulnerable actions
124+
* Fields:
125+
* - action: action name
126+
*/
127+
predicate immutableActionsDataModel(
128+
string action
129+
) {
130+
Extensions::immutableActionsDataModel(action)
131+
}
132+
122133
/**
123134
* MaD models for untrusted git commands
124135
* Fields:

ql/lib/codeql/actions/config/ConfigExtensions.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ extensible predicate vulnerableActionsDataModel(
5858
string action, string vulnerable_version, string vulnerable_sha, string fixed_version
5959
);
6060

61+
/**
62+
* Holds for actions that are known to be immutable.
63+
*/
64+
extensible predicate immutableActionsDataModel(
65+
string action
66+
);
67+
6168
/**
6269
* Holds for git commands that may introduce untrusted data when called on an attacker controlled branch.
6370
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
extensions:
2+
- addsTo:
3+
pack: github/actions-all
4+
extensible: immutableActionsDataModel
5+
data:
6+
- ["actions/checkout"]
7+
- ["actions/cache"]
8+
- ["actions/setup-node"]
9+
- ["actions/upload-artifact"]
10+
- ["actions/setup-python"]
11+
- ["actions/download-artifact"]
12+
- ["actions/github-script"]
13+
- ["actions/setup-java"]
14+
- ["actions/setup-go"]
15+
- ["actions/upload-pages-artifact"]
16+
- ["actions/deploy-pages"]
17+
- ["actions/setup-dotnet"]
18+
- ["actions/stale"]
19+
- ["actions/labeler"]
20+
- ["actions/create-github-app-token"]
21+
- ["actions/configure-pages"]
22+
- ["octokit/request-action"]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Unpinned tag for 3rd party Action in workflow
2+
3+
## Description
4+
5+
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.
6+
7+
## Recommendations
8+
9+
Pinning an action to a full length commit SHA is currently the only way to use an 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.
10+
11+
## Examples
12+
13+
### Incorrect Usage
14+
15+
```yaml
16+
- uses: tj-actions/changed-files@v44
17+
```
18+
19+
### Correct Usage
20+
21+
```yaml
22+
- uses: tj-actions/changed-files@c65cd883420fd2eb864698a825fc4162dd94482c # v44
23+
```
24+
25+
## References
26+
27+
- [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)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @name Unversioned Immutable Action
3+
* @description Using an Immutable Action without a semantic version tag opts out of the protections of Immutable Action
4+
* @kind problem
5+
* @security-severity 5.0
6+
* @problem.severity recommendation
7+
* @precision high
8+
* @id actions/unversioned-immutable-action
9+
* @tags security
10+
* actions
11+
* external/cwe/cwe-829
12+
*/
13+
14+
import actions
15+
16+
bindingset[version]
17+
private predicate isSemanticVersioned(string version) { version.regexpMatch("^v[0-9]+(\\.[0-9]+)*(\\.[xX])?$") }
18+
19+
bindingset[repo]
20+
private predicate isTrustedOrg(string repo) {
21+
exists(string org | org in ["actions", "github", "advanced-security", "octokit"] | repo.matches(org + "/%"))
22+
}
23+
24+
from UsesStep uses, string repo, string version, Workflow workflow, string name
25+
where
26+
uses.getCallee() = repo and
27+
uses.getEnclosingWorkflow() = workflow and
28+
(
29+
workflow.getName() = name
30+
or
31+
not exists(workflow.getName()) and workflow.getLocation().getFile().getBaseName() = name
32+
) and
33+
uses.getVersion() = version and
34+
not isTrustedOrg(repo) and
35+
not isPinnedCommit(version)
36+
select uses.getCalleeNode(),
37+
"Unpinned 3rd party Action '" + name + "' step $@ uses '" + repo + "' with ref '" + version +
38+
"', not a pinned commit hash", uses, uses.toString()

0 commit comments

Comments
 (0)