diff --git a/ql/lib/codeql/actions/dataflow/FlowSources.qll b/ql/lib/codeql/actions/dataflow/FlowSources.qll index 56c901434ce..e0d46c7196d 100644 --- a/ql/lib/codeql/actions/dataflow/FlowSources.qll +++ b/ql/lib/codeql/actions/dataflow/FlowSources.qll @@ -90,7 +90,8 @@ class GitCommandSource extends RemoteFlowSource, CommandSource { checkout = uses and uses.getCallee() = "actions/checkout" and exists(uses.getArgument("ref")) and - not uses.getArgument("ref").matches("%base%") + not uses.getArgument("ref").matches("%base%") and + uses.getEnclosingJob().getATriggerEvent().getName() = checkoutTriggers() ) or checkout instanceof GitMutableRefCheckout @@ -237,7 +238,8 @@ private class CheckoutSource extends RemoteFlowSource, FileSource { this.asExpr() = uses and uses.getCallee() = "actions/checkout" and exists(uses.getArgument("ref")) and - not uses.getArgument("ref").matches("%base%") + not uses.getArgument("ref").matches("%base%") and + uses.getEnclosingJob().getATriggerEvent().getName() = checkoutTriggers() ) or this.asExpr() instanceof GitMutableRefCheckout diff --git a/ql/lib/codeql/actions/security/OutputClobberingQuery.qll b/ql/lib/codeql/actions/security/OutputClobberingQuery.qll index 311c3abdb69..5850aa91e6e 100644 --- a/ql/lib/codeql/actions/security/OutputClobberingQuery.qll +++ b/ql/lib/codeql/actions/security/OutputClobberingQuery.qll @@ -29,7 +29,8 @@ class OutputClobberingFromFileReadSink extends OutputClobberingSink { step = uses and uses.getCallee() = "actions/checkout" and exists(uses.getArgument("ref")) and - not uses.getArgument("ref").matches("%base%") + not uses.getArgument("ref").matches("%base%") and + uses.getEnclosingJob().getATriggerEvent().getName() = checkoutTriggers() ) or step instanceof GitMutableRefCheckout diff --git a/ql/lib/codeql/actions/security/PoisonableSteps.qll b/ql/lib/codeql/actions/security/PoisonableSteps.qll index 99d844bae79..d446c446641 100644 --- a/ql/lib/codeql/actions/security/PoisonableSteps.qll +++ b/ql/lib/codeql/actions/security/PoisonableSteps.qll @@ -17,12 +17,13 @@ class PoisonableCommandStep extends PoisonableStep, Run { class JavascriptImportUsesStep extends PoisonableStep, UsesStep { JavascriptImportUsesStep() { - exists(string script, string line, string import_stmt | + exists(string script, string line | this.getCallee() = "actions/github-script" and script = this.getArgument("script") and line = script.splitAt("\n").trim() and - import_stmt = line.regexpCapture(".*await\\s+import\\((.*)\\).*", 1) and - import_stmt.regexpMatch(".*\\bgithub.workspace\\b.*") + // const script = require('${{ github.workspace }}/scripts/test.js'); + // await script({ github, context, core }); + line.regexpMatch(".*(import|require)\\b.*github.workspace\\b.*") ) } } diff --git a/ql/lib/codeql/actions/security/UntrustedCheckoutQuery.qll b/ql/lib/codeql/actions/security/UntrustedCheckoutQuery.qll index 336afdc73b1..621f4b80e1f 100644 --- a/ql/lib/codeql/actions/security/UntrustedCheckoutQuery.qll +++ b/ql/lib/codeql/actions/security/UntrustedCheckoutQuery.qll @@ -3,49 +3,57 @@ private import codeql.actions.DataFlow private import codeql.actions.dataflow.FlowSources private import codeql.actions.TaintTracking +string checkoutTriggers() { + result = ["pull_request_target", "workflow_run", "workflow_call", "issue_comment"] +} + /** * A taint-tracking configuration for PR HEAD references flowing * into actions/checkout's ref argument. */ private module ActionsMutableRefCheckoutConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { - // remote flow sources - source instanceof ArtifactSource - or - source instanceof GitHubCtxSource - or - source instanceof GitHubEventCtxSource - or - source instanceof GitHubEventJsonSource - or - source instanceof MaDSource - or - // `ref` argument contains the PR id/number or head ref - exists(Expression e | - source.asExpr() = e and - ( - containsHeadRef(e.getExpression()) or - containsPullRequestNumber(e.getExpression()) + source.asExpr().getEnclosingJob().getATriggerEvent().getName() = checkoutTriggers() and + ( + // remote flow sources + source instanceof ArtifactSource + or + source instanceof GitHubCtxSource + or + source instanceof GitHubEventCtxSource + or + source instanceof GitHubEventJsonSource + or + source instanceof MaDSource + or + // `ref` argument contains the PR id/number or head ref + exists(Expression e | + source.asExpr() = e and + ( + containsHeadRef(e.getExpression()) or + containsPullRequestNumber(e.getExpression()) + ) ) - ) - or - // 3rd party actions returning the PR head ref - exists(StepsExpression e, UsesStep step | - source.asExpr() = e and - e.getStepId() = step.getId() and - ( - step.getCallee() = "eficode/resolve-pr-refs" and e.getFieldName() = "head_ref" - or - step.getCallee() = "xt0rted/pull-request-comment-branch" and e.getFieldName() = "head_ref" - or - step.getCallee() = "alessbell/pull-request-comment-branch" and e.getFieldName() = "head_ref" - or - step.getCallee() = "gotson/pull-request-comment-branch" and e.getFieldName() = "head_ref" - or - step.getCallee() = "potiuk/get-workflow-origin" and - e.getFieldName() = ["sourceHeadBranch", "pullRequestNumber"] - or - step.getCallee() = "github/branch-deploy" and e.getFieldName() = ["ref", "fork_ref"] + or + // 3rd party actions returning the PR head ref + exists(StepsExpression e, UsesStep step | + source.asExpr() = e and + e.getStepId() = step.getId() and + ( + step.getCallee() = "eficode/resolve-pr-refs" and e.getFieldName() = "head_ref" + or + step.getCallee() = "xt0rted/pull-request-comment-branch" and e.getFieldName() = "head_ref" + or + step.getCallee() = "alessbell/pull-request-comment-branch" and + e.getFieldName() = "head_ref" + or + step.getCallee() = "gotson/pull-request-comment-branch" and e.getFieldName() = "head_ref" + or + step.getCallee() = "potiuk/get-workflow-origin" and + e.getFieldName() = ["sourceHeadBranch", "pullRequestNumber"] + or + step.getCallee() = "github/branch-deploy" and e.getFieldName() = ["ref", "fork_ref"] + ) ) ) } @@ -71,27 +79,32 @@ module ActionsMutableRefCheckoutFlow = TaintTracking::Global> $GITHUB_OUTPUT + if [[ ${{ github.event.inputs.version }} == 'stable' ]]; then + NEW_VERSION=$(npx semver $OLD_VERSION -i patch) + else + if [[ $OLD_VERSION == *"rc"* ]]; then + NEW_VERSION=$(npx semver $OLD_VERSION -i prerelease) + else + # WordPress version guidelines: If minor is 9, bump major instead. + IFS='.' read -r -a OLD_VERSION_ARRAY <<< "$OLD_VERSION" + if [[ ${OLD_VERSION_ARRAY[1]} == "9" ]]; then + NEW_VERSION="$(npx semver $OLD_VERSION -i major)-rc.1" + else + NEW_VERSION="$(npx semver $OLD_VERSION -i minor)-rc.1" + fi + fi + fi + echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT + IFS='.' read -r -a NEW_VERSION_ARRAY <<< "$NEW_VERSION" + RELEASE_BRANCH="release/${NEW_VERSION_ARRAY[0]}.${NEW_VERSION_ARRAY[1]}" + echo "release_branch=${RELEASE_BRANCH}" >> $GITHUB_OUTPUT + + build: + runs-on: ubuntu-latest + needs: bump-version + if: | + always() && ( + github.event_name == 'pull_request' || + github.event_name == 'workflow_dispatch' || + github.repository == 'test/test' + ) + steps: + - name: Checkout code + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + ref: ${{ needs.bump-version.outputs.release_branch || github.ref }} + + - run: ./bin/build-plugin-zip.sh diff --git a/ql/test/query-tests/Security/CWE-829/.github/workflows/test23.yml b/ql/test/query-tests/Security/CWE-829/.github/workflows/test23.yml new file mode 100644 index 00000000000..da889dd2ac6 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/.github/workflows/test23.yml @@ -0,0 +1,47 @@ +on: + schedule: + - cron: "0 3 * * 2-6" # Tuesdays - Saturdays, at 3am UTC + workflow_dispatch: + inputs: + pr: + description: "PR Number" + required: false + type: number + release: + types: [ published ] + +jobs: + resolve-required-data: + name: Resolve Required Data + if: ${{ github.repository_owner == 'test' }} + runs-on: ubuntu-latest + outputs: + ref: ${{ steps.script.outputs.ref }} + steps: + - name: Resolve and set checkout and version data to use for release + id: script + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ github.event.inputs.pr }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('${{ github.workspace }}/scripts/publish-resolve-data.js'); + await script({ github, context, core }); + + build: + needs: [ resolve-required-data ] + if: ${{ github.repository_owner == 'test' }} + name: stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ needs.resolve-required-data.outputs.repo }} + ref: ${{ needs.resolve-required-data.outputs.ref }} + + - name: Build + shell: bash + run: | + ./cmd + diff --git a/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected b/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected index 237928fc892..339cd5f6cf4 100644 --- a/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected +++ b/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected @@ -267,6 +267,9 @@ edges | .github/workflows/test18.yml:33:15:36:12 | Run Step | .github/workflows/test18.yml:36:15:40:58 | Uses Step | | .github/workflows/test19.yml:16:7:21:4 | Uses Step | .github/workflows/test19.yml:21:7:22:14 | Run Step | | .github/workflows/test20.yml:16:7:21:4 | Uses Step | .github/workflows/test20.yml:21:7:22:14 | Run Step | +| .github/workflows/test21.yml:18:9:25:6 | Uses Step | .github/workflows/test21.yml:25:9:27:36 | Run Step | +| .github/workflows/test22.yml:57:15:62:12 | Uses Step | .github/workflows/test22.yml:62:15:62:45 | Run Step | +| .github/workflows/test23.yml:38:9:43:6 | Uses Step | .github/workflows/test23.yml:43:9:46:16 | Run Step | | .github/workflows/test.yml:13:9:14:6 | Uses Step | .github/workflows/test.yml:14:9:25:6 | Run Step | | .github/workflows/test.yml:14:9:25:6 | Run Step | .github/workflows/test.yml:25:9:33:6 | Run Step | | .github/workflows/test.yml:25:9:33:6 | Run Step | .github/workflows/test.yml:33:9:37:34 | Run Step | diff --git a/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutHigh.expected b/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutHigh.expected index 13e16280c33..1d6122b3747 100644 --- a/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutHigh.expected +++ b/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutHigh.expected @@ -1,7 +1,3 @@ -| .github/workflows/issue_comment_3rd_party_action.yml:16:9:22:2 | Uses Step | Potential execution of untrusted code on a privileged workflow. | -| .github/workflows/issue_comment_3rd_party_action.yml:30:9:36:2 | Uses Step | Potential execution of untrusted code on a privileged workflow. | -| .github/workflows/issue_comment_3rd_party_action.yml:45:9:49:6 | Uses Step | Potential execution of untrusted code on a privileged workflow. | -| .github/workflows/issue_comment_3rd_party_action.yml:49:9:52:25 | Uses Step | Potential execution of untrusted code on a privileged workflow. | | .github/workflows/issue_comment_direct.yml:12:9:16:2 | Uses Step | Potential execution of untrusted code on a privileged workflow. | | .github/workflows/issue_comment_direct.yml:20:9:24:2 | Uses Step | Potential execution of untrusted code on a privileged workflow. | | .github/workflows/issue_comment_direct.yml:28:9:32:2 | Uses Step | Potential execution of untrusted code on a privileged workflow. | diff --git a/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected b/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected index c81666f72dc..a476bdc22d8 100644 --- a/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected +++ b/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected @@ -1,13 +1,6 @@ -| .github/reusable_workflows/TestOrg/TestRepo/.github/workflows/formal.yml:14:9:19:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | | .github/workflows/artifactpoisoning81.yml:11:9:14:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/artifactpoisoning82.yml:11:9:14:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/dependabot1.yml:15:9:19:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/dependabot1.yml:39:9:43:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/dependabot2.yml:33:9:38:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/mend.yml:22:9:29:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | | .github/workflows/poc3.yml:18:7:25:4 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | | .github/workflows/poc.yml:30:9:36:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/priv_pull_request_checkout.yml:14:9:20:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | | .github/workflows/test3.yml:28:9:33:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | | .github/workflows/test4.yml:18:7:25:4 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | | .github/workflows/test8.yml:20:9:26:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. |