Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added a new extensible predicate `pinnedByLockfileDataModel(workflow_path, nwo, ref)`, which lets model packs record `uses:` references pinned by a repository's Actions lockfile (`.github/workflows/actions.lock`).
20 changes: 20 additions & 0 deletions actions/ql/lib/codeql/actions/config/Config.qll
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,26 @@ predicate trustedActionsOwnerDataModel(string owner) {
Extensions::trustedActionsOwnerDataModel(owner)
}

/**
* MaD models for `uses` references pinned by the repository's Actions lockfile
* (`.github/workflows/actions.lock`).
* Fields:
* - workflow_path: repo-relative path of the file containing the `uses:` reference
* - nwo: referenced action, optionally including a sub-action path (e.g. `actions/cache/save`)
* - ref: the ref as written in `uses:` (e.g. `v4`)
*/
bindingset[nwo]
predicate pinnedByLockfileDataModel(string workflow_path, string nwo, string ref) {
exists(string pinnedNwo |
Extensions::pinnedByLockfileDataModel(workflow_path, pinnedNwo, ref) and
(
nwo.toLowerCase() = pinnedNwo
or
nwo.toLowerCase().prefix(pinnedNwo.length() + 1) = pinnedNwo + "/"
)
)
}

/**
* MaD models for untrusted git commands
* Fields:
Expand Down
17 changes: 17 additions & 0 deletions actions/ql/lib/codeql/actions/config/ConfigExtensions.qll
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ extensible predicate immutableActionsDataModel(string action);
*/
extensible predicate trustedActionsOwnerDataModel(string owner);

/**
* Holds if the `uses` reference `nwo`@`ref` in the workflow or composite action file at
* `workflow_path` is pinned by an entry in the repository's Actions lockfile
* (`.github/workflows/actions.lock`).
*
* Supply rows from a model pack generated with the canonical parser at
* `github.com/github/actions-lockfile/go/pkg/lockfile`. Each lockfile entry binds an `nwo`@`ref`
* to a verified commit SHA. Without such a model pack this predicate is empty.
*
* Fields:
* - `workflow_path`: repo-relative path of the file containing the `uses:` reference,
* e.g. `.github/workflows/ci.yml`.
* - `nwo`: canonical owner and repository from the lockfile pin, e.g. `actions/cache`.
* - `ref`: the ref (tag or branch) as written in `uses:`, e.g. `v4`.
*/
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
extensible predicate pinnedByLockfileDataModel(string workflow_path, string nwo, string ref);

/**
* Holds for git commands that may introduce untrusted data when called on an attacker controlled branch.
*/
Expand Down
11 changes: 11 additions & 0 deletions actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ private predicate isPinnedContainer(string version) {
bindingset[nwo]
private predicate isContainerImage(string nwo) { nwo.regexpMatch("^docker://.+") }

// A `$/` reference is a same-repository (self repository) reference (e.g. `$/path/to/action`),
// resolved at the commit the calling workflow is running. Like `./` local (self workspace)
// references, it is inherently pinned and can never be an unpinned-tag finding, so we never flag it.
bindingset[nwo]
private predicate isSelfRepository(string nwo) { nwo.matches("$/%") }

private predicate hasUsesContainerName(Uses uses, string name) {
exists(Workflow workflow |
uses.getEnclosingWorkflow() = workflow and
Expand All @@ -55,6 +61,11 @@ where
hasUsesContainerName(uses, name) and
uses.getVersion() = version and
not isTrustedOwner(nwo) and
not isSelfRepository(nwo) and
not exists(UsesStep step |
uses = step and
pinnedByLockfileDataModel(step.getLocation().getFile().getRelativePath(), nwo, version)
) and
not (
if uses instanceof UsesStep and isContainerImage(nwo)
then isPinnedContainer(version)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `actions/unpinned-tag` query no longer reports `uses:` references recorded by the new `pinnedByLockfileDataModel` extensible predicate. Lockfile pins match repository sub-actions while preserving ref casing.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `actions/unpinned-tag` query no longer reports `$/` self repository references (e.g. `uses: $/path/to/action`), which resolve to the same repository at the running commit and are therefore inherently pinned, just like `./` self workspace (local) references.
1 change: 1 addition & 0 deletions actions/ql/test/qlpack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ extractor: actions
tests: .
warnOnImplicitThis: true
dataExtensions:
- query-tests/Security/CWE-829/*.model.yml
- output-clobbering.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
on:
pull_request

jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
# `some-owner/pinned-action@v1` is a tag ref that would normally be reported as an unpinned
# tag. The test data extension `pinned_by_lockfile.model.yml` records it as pinned by the
# repository's Actions lockfile, so the `not pinnedByLockfile(...)` clause suppresses it (this
# fixture is expected to produce no findings). This mirrors data supplied by an external
# model pack generated from `.github/workflows/actions.lock`.
#
# Negative control is provided for free by the many other fixtures in this directory whose tag
# refs are NOT recorded in the data extension and therefore remain reported.
- uses: some-owner/pinned-action@v1
# `Mixed-Owner/Pinned-Action@v1` is pinned by the lockfile too, but written with the mixed-case
# owner/repo that authors commonly use (Azure, GoogleCloudPlatform, ...).
# Lockfile pins canonicalize owner/repo, so this ref is also expected to be suppressed (no
# finding).
- uses: Mixed-Owner/Pinned-Action@v1
# Negative control: a mixed-case ref that is NOT recorded in the lockfile data must still be
# reported, guarding against over-suppression of every mixed-case ref.
- uses: Mixed-Owner/Unpinned-Action@v2
# A repository-scoped pin also covers its sub-actions.
- uses: some-owner/pinned-action/save@v1
# Ref casing must match the canonical lockfile pin exactly.
- uses: some-owner/pinned-action@V1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
on:
pull_request

jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
# `$/` is a same-repository (self repository) reference resolved at the running commit. It is
# inherently pinned (like `./` self workspace refs) and must never be reported as an unpinned tag.
- uses: $/actions/foo
# `$/…@ref` is rejected by the `$/` rule, but a user could still write it. It must also
# never be flagged; this case exercises the `not isSelfRepository(nwo)` suppression, since
# without it `$/actions/foo@v1` would otherwise be reported as an unpinned tag.
- uses: $/actions/foo@v1
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
| .github/workflows/label_trusted_checkout2.yml:21:13:21:36 | completely/fakeaction@v2 | Unpinned 3rd party Action 'label_trusted_checkout2.yml' step $@ uses 'completely/fakeaction' with ref 'v2', not a pinned commit hash | .github/workflows/label_trusted_checkout2.yml:21:7:25:4 | Uses Step | Uses Step |
| .github/workflows/label_trusted_checkout2.yml:25:13:25:37 | fakerepo/comment-on-pr@v1 | Unpinned 3rd party Action 'label_trusted_checkout2.yml' step $@ uses 'fakerepo/comment-on-pr' with ref 'v1', not a pinned commit hash | .github/workflows/label_trusted_checkout2.yml:25:7:28:21 | Uses Step | Uses Step |
| .github/workflows/level0.yml:36:15:36:47 | rlespinasse/github-slug-action@v4 | Unpinned 3rd party Action 'Poutine Level 0' step $@ uses 'rlespinasse/github-slug-action' with ref 'v4', not a pinned commit hash | .github/workflows/level0.yml:36:9:39:6 | Uses Step | Uses Step |
| .github/workflows/lockfile_pinned.yml:25:13:25:42 | Mixed-Owner/Unpinned-Action@v2 | Unpinned 3rd party Action 'lockfile_pinned.yml' step $@ uses 'Mixed-Owner/Unpinned-Action' with ref 'v2', not a pinned commit hash | .github/workflows/lockfile_pinned.yml:25:7:27:4 | Uses Step | Uses Step |
| .github/workflows/lockfile_pinned.yml:29:13:29:39 | some-owner/pinned-action@V1 | Unpinned 3rd party Action 'lockfile_pinned.yml' step $@ uses 'some-owner/pinned-action' with ref 'V1', not a pinned commit hash | .github/workflows/lockfile_pinned.yml:29:7:29:40 | Uses Step | Uses Step |
| .github/workflows/mend.yml:31:15:31:34 | ruby/setup-ruby@v1 | Unpinned 3rd party Action 'Test' step $@ uses 'ruby/setup-ruby' with ref 'v1', not a pinned commit hash | .github/workflows/mend.yml:29:9:33:28 | Uses Step | Uses Step |
| .github/workflows/pr-workflow.yml:60:15:60:52 | amannn/action-semantic-pull-request@v5 | Unpinned 3rd party Action 'pr-workflow' step $@ uses 'amannn/action-semantic-pull-request' with ref 'v5', not a pinned commit hash | .github/workflows/pr-workflow.yml:60:9:70:6 | Uses Step | Uses Step |
| .github/workflows/pr-workflow.yml:109:15:109:42 | actionsdesk/lfs-warning@v3.2 | Unpinned 3rd party Action 'pr-workflow' step $@ uses 'actionsdesk/lfs-warning' with ref 'v3.2', not a pinned commit hash | .github/workflows/pr-workflow.yml:109:9:124:6 | Uses Step | Uses Step |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ edges
| .github/workflows/level0.yml:125:9:129:6 | Uses Step | .github/workflows/level0.yml:129:9:133:6 | Uses Step |
| .github/workflows/level0.yml:128:17:128:57 | github.event.pull_request.head.sha | .github/workflows/level0.yml:125:9:129:6 | Uses Step |
| .github/workflows/level0.yml:129:9:133:6 | Uses Step | .github/workflows/level0.yml:133:9:135:23 | Run Step |
| .github/workflows/lockfile_pinned.yml:17:7:22:4 | Uses Step | .github/workflows/lockfile_pinned.yml:22:7:25:4 | Uses Step |
| .github/workflows/lockfile_pinned.yml:22:7:25:4 | Uses Step | .github/workflows/lockfile_pinned.yml:25:7:27:4 | Uses Step |
| .github/workflows/lockfile_pinned.yml:25:7:27:4 | Uses Step | .github/workflows/lockfile_pinned.yml:27:7:29:4 | Uses Step |
| .github/workflows/lockfile_pinned.yml:27:7:29:4 | Uses Step | .github/workflows/lockfile_pinned.yml:29:7:29:40 | Uses Step |
| .github/workflows/mend.yml:13:9:22:6 | Run Step: set_ref | .github/workflows/mend.yml:22:9:29:6 | Uses Step |
| .github/workflows/mend.yml:22:9:29:6 | Uses Step | .github/workflows/mend.yml:29:9:33:28 | Uses Step |
| .github/workflows/mend.yml:27:17:27:48 | steps.set_ref.outputs.ref | .github/workflows/mend.yml:22:9:29:6 | Uses Step |
Expand Down Expand Up @@ -241,6 +245,7 @@ edges
| .github/workflows/resolve-args.yml:20:9:22:6 | Uses Step | .github/workflows/resolve-args.yml:22:9:36:13 | Run Step: resolve-step |
| .github/workflows/reusable_local.yml:23:9:26:6 | Uses Step | .github/workflows/reusable_local.yml:26:9:29:7 | Run Step |
| .github/workflows/reusable_local.yml:25:17:25:36 | inputs.branch | .github/workflows/reusable_local.yml:23:9:26:6 | Uses Step |
| .github/workflows/self_ref_dollar.yml:11:7:15:4 | Uses Step | .github/workflows/self_ref_dollar.yml:15:7:15:29 | Uses Step |
| .github/workflows/test1.yml:18:9:21:6 | Uses Step | .github/workflows/test1.yml:21:9:24:6 | Run Step |
| .github/workflows/test1.yml:21:9:24:6 | Run Step | .github/workflows/test1.yml:24:9:25:39 | Run Step |
| .github/workflows/test2.yml:13:9:16:6 | Uses Step | .github/workflows/test2.yml:16:9:20:52 | Uses Step |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extensions:
- addsTo:
pack: codeql/actions-all
extensible: pinnedByLockfileDataModel
# Canonical lockfile pin rows supplied by an external model pack.
data:
- [".github/workflows/lockfile_pinned.yml", "some-owner/pinned-action", "v1"]
# Owner/repo is canonicalized; ref casing is preserved.
- [".github/workflows/lockfile_pinned.yml", "mixed-owner/pinned-action", "v1"]
Loading