diff --git a/actions/ql/examples/snippets/uses_pinned_sha.ql b/actions/ql/examples/snippets/uses_pinned_sha.ql index 84b2cdae0fdb..db055a69fc4f 100644 --- a/actions/ql/examples/snippets/uses_pinned_sha.ql +++ b/actions/ql/examples/snippets/uses_pinned_sha.ql @@ -8,5 +8,5 @@ import actions from UsesStep uses -where uses.getVersion().regexpMatch("^[A-Fa-f0-9]{40}$") +where uses.getVersion().regexpMatch("^[A-Fa-f0-9]{40}([A-Fa-f0-9]{24})?$") select uses, "This 'uses' step has a pinned SHA version." diff --git a/actions/ql/lib/change-notes/2026-05-12-improved-alphanumeric-regex.md b/actions/ql/lib/change-notes/2026-05-12-improved-alphanumeric-regex.md new file mode 100644 index 000000000000..df3aaf3613fa --- /dev/null +++ b/actions/ql/lib/change-notes/2026-05-12-improved-alphanumeric-regex.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The GitHub Actions analysis now recognizes more Bash regex checks that restrict a value to alphanumeric characters, include regexes like `^[0-9a-zA-Z]{40}([0-9a-zA-Z]{24})?$` which check for a sha1 or sha256 hash. This may reduce false positive results where command output is validated with grouped or optional alphanumeric patterns before being used. \ No newline at end of file diff --git a/actions/ql/lib/codeql/actions/Bash.qll b/actions/ql/lib/codeql/actions/Bash.qll index 4975ce6f4cc5..14ba8f0acf4a 100644 --- a/actions/ql/lib/codeql/actions/Bash.qll +++ b/actions/ql/lib/codeql/actions/Bash.qll @@ -785,7 +785,22 @@ module Bash { /** * Holds if the given regex is used to match an alphanumeric string - * eg: `^[0-9a-zA-Z]{40}$`, `^[0-9]+$` or `^[a-zA-Z0-9_]+$` + * eg: `^[0-9a-zA-Z]{40}([0-9a-zA-Z]{24})?$`, `^[0-9]+$` or `^[a-zA-Z0-9_]+$` */ - string alphaNumericRegex() { result = "^\\^\\[([09azAZ_-]+)\\](\\+|\\{\\d+\\})\\$$" } + string alphaNumericRegex() { + exists(string r1, string r2, string r3, string r4 | + // An alphanumeric character class + r1 = "\\[([09azAZ_-]+)\\]" and + // The same as above, followed by a quantifier like `+` or `{20}` + r2 = r1 + "(\\+|\\{\\d+\\})" and + // The same as above, possibly with parentheses around it + r3 = "\\(?" + r2 + "\\)?" and + // The same as above, possibly with a `?` after it + r4 = r3 + "\\??" + | + // The same as above, repeated one or more times, and with `^` at the + // beginning and `$` at the end + result = "^\\^(" + r4 + ")+\\$$" + ) + } } diff --git a/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql b/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql index c8512e5a1c0c..283680752020 100644 --- a/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql +++ b/actions/ql/src/Security/CWE-829/UnpinnedActionsTag.ql @@ -15,7 +15,9 @@ import actions import codeql.actions.security.UseOfUnversionedImmutableAction bindingset[version] -private predicate isPinnedCommit(string version) { version.regexpMatch("^[A-Fa-f0-9]{40}$") } +private predicate isPinnedCommit(string version) { + version.regexpMatch("^[A-Fa-f0-9]{40}([A-Fa-f0-9]{24})?$") +} bindingset[nwo] private predicate isTrustedOwner(string nwo) { diff --git a/actions/ql/src/change-notes/2026-05-12-sha256-pinned-actions.md b/actions/ql/src/change-notes/2026-05-12-sha256-pinned-actions.md new file mode 100644 index 000000000000..521a5878c371 --- /dev/null +++ b/actions/ql/src/change-notes/2026-05-12-sha256-pinned-actions.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `actions/unpinned-tag` query now recognizes 64-character SHA-256 commit hashes as properly pinned references, in addition to 40-character SHA-1 hashes. diff --git a/actions/ql/test/query-tests/Security/CWE-829/.github/workflows/unpinned_tags.yml b/actions/ql/test/query-tests/Security/CWE-829/.github/workflows/unpinned_tags.yml index f204816eed4e..6e7612144bcc 100644 --- a/actions/ql/test/query-tests/Security/CWE-829/.github/workflows/unpinned_tags.yml +++ b/actions/ql/test/query-tests/Security/CWE-829/.github/workflows/unpinned_tags.yml @@ -11,3 +11,9 @@ jobs: - uses: foo/bar@25b062c917b0c75f8b47d8469aff6c94ffd89abb - uses: docker://foo/bar@latest - uses: docker://foo/bar@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 + # SHA-256 pinned (64 hex chars) - should NOT be flagged + - uses: foo/bar@25b062c917b0c75f8b47d8469aff6c94ffd89abb25b062c917b0c75f8b47d84d + # SHA-1 pinned (40 hex chars) regression - should NOT be flagged + - uses: foo/bar@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 + # Invalid 50-char hex string - should be flagged + - uses: foo/bar@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2a1b2c3d4e5 diff --git a/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected b/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected index ed35f1546171..b55345c8f77a 100644 --- a/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected +++ b/actions/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected @@ -33,3 +33,4 @@ | .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 | | .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 | | .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 | +| .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 | diff --git a/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected b/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected index 39e54b2bbaed..7cf5657d2e80 100644 --- a/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected +++ b/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected @@ -311,7 +311,10 @@ edges | .github/workflows/unpinned_tags.yml:9:7:10:4 | Uses Step | .github/workflows/unpinned_tags.yml:10:7:11:4 | Uses Step | | .github/workflows/unpinned_tags.yml:10:7:11:4 | Uses Step | .github/workflows/unpinned_tags.yml:11:7:12:4 | Uses Step | | .github/workflows/unpinned_tags.yml:11:7:12:4 | Uses Step | .github/workflows/unpinned_tags.yml:12:7:13:4 | Uses Step | -| .github/workflows/unpinned_tags.yml:12:7:13:4 | Uses Step | .github/workflows/unpinned_tags.yml:13:7:13:101 | Uses Step | +| .github/workflows/unpinned_tags.yml:12:7:13:4 | Uses Step | .github/workflows/unpinned_tags.yml:13:7:15:4 | Uses Step | +| .github/workflows/unpinned_tags.yml:13:7:15:4 | Uses Step | .github/workflows/unpinned_tags.yml:15:7:17:4 | Uses Step | +| .github/workflows/unpinned_tags.yml:15:7:17:4 | Uses Step | .github/workflows/unpinned_tags.yml:17:7:19:4 | Uses Step | +| .github/workflows/unpinned_tags.yml:17:7:19:4 | Uses Step | .github/workflows/unpinned_tags.yml:19:7:19:71 | Uses Step | | .github/workflows/untrusted_checkout2.yml:7:9:14:6 | Run Step: pr_number | .github/workflows/untrusted_checkout2.yml:14:9:19:72 | Run Step | | .github/workflows/untrusted_checkout3.yml:11:9:12:6 | Uses Step | .github/workflows/untrusted_checkout3.yml:12:9:13:6 | Uses Step | | .github/workflows/untrusted_checkout3.yml:12:9:13:6 | Uses Step | .github/actions/dangerous-git-checkout/action.yml:6:7:11:4 | Uses Step |