diff --git a/ql/lib/codeql/actions/Ast.qll b/ql/lib/codeql/actions/Ast.qll index ecc0ad16f5f..d865eb54905 100644 --- a/ql/lib/codeql/actions/Ast.qll +++ b/ql/lib/codeql/actions/Ast.qll @@ -38,7 +38,9 @@ class AstNode instanceof AstNodeImpl { Expression getInScopeEnvVarExpr(string name) { result = super.getInScopeEnvVarExpr(name) } } -class ScalarValue extends AstNode instanceof ScalarValueImpl { } +class ScalarValue extends AstNode instanceof ScalarValueImpl { + string getValue() { result = super.getValue() } +} class Expression extends AstNode instanceof ExpressionImpl { string expression; @@ -218,6 +220,8 @@ abstract class Uses extends AstNode instanceof UsesImpl { string getVersion() { result = super.getVersion() } + string getArgument(string argName) { result = super.getArgument(argName) } + Expression getArgumentExpr(string argName) { result = super.getArgumentExpr(argName) } } diff --git a/ql/lib/codeql/actions/ast/internal/Ast.qll b/ql/lib/codeql/actions/ast/internal/Ast.qll index 3fa1769e762..a1470a41dd0 100644 --- a/ql/lib/codeql/actions/ast/internal/Ast.qll +++ b/ql/lib/codeql/actions/ast/internal/Ast.qll @@ -128,6 +128,8 @@ class ScalarValueImpl extends AstNodeImpl, TScalarValueNode { override Location getLocation() { result = value.getLocation() } override YamlScalar getNode() { result = value } + + string getValue() { result = value.getValue() } } class ExpressionImpl extends AstNodeImpl, TExpressionNode { @@ -687,7 +689,19 @@ abstract class UsesImpl extends AstNodeImpl { abstract string getVersion(); - abstract ExpressionImpl getArgumentExpr(string key); + /** Gets the argument expression for the given key. */ + string getArgument(string key) { + exists(ScalarValueImpl scalar | + scalar.getNode() = this.getNode().(YamlMapping).lookup("with").(YamlMapping).lookup(key) and + result = scalar.getValue() + ) + } + + /** Gets the argument expression for the given key (if it exists). */ + ExpressionImpl getArgumentExpr(string key) { + result.getParentNode().getNode() = + this.getNode().(YamlMapping).lookup("with").(YamlMapping).lookup(key) + } } /** @@ -719,11 +733,6 @@ class UsesStepImpl extends StepImpl, UsesImpl { /** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */ override string getVersion() { result = u.getValue().regexpCapture(usesParser(), 3) } - /** Gets the argument expression for the given key. */ - override ExpressionImpl getArgumentExpr(string key) { - result.getParentNode().getNode() = n.lookup("with").(YamlMapping).lookup(key) - } - override string toString() { if exists(this.getId()) then result = "Uses Step: " + this.getId() else result = "Uses Step" } @@ -763,11 +772,6 @@ class ExternalJobImpl extends JobImpl, UsesImpl { else none() ) } - - /** Gets the argument expression for the given key. */ - override ExpressionImpl getArgumentExpr(string key) { - result.getParentNode().getNode() = n.lookup("with").(YamlMapping).lookup(key) - } } class RunImpl extends StepImpl { diff --git a/ql/lib/codeql/actions/dataflow/FlowSteps.qll b/ql/lib/codeql/actions/dataflow/FlowSteps.qll index c10334436aa..34357816812 100644 --- a/ql/lib/codeql/actions/dataflow/FlowSteps.qll +++ b/ql/lib/codeql/actions/dataflow/FlowSteps.qll @@ -33,12 +33,12 @@ class AdditionalTaintStep extends Unit { * echo "foo=$(echo $BODY)" >> $GITHUB_OUTPUT * echo "foo=$(echo $BODY)" >> "$GITHUB_OUTPUT" */ -predicate runEnvToScriptStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) { - exists(Run r, string varName, string output | +predicate envToOutputStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) { + exists(Run run, string varName, string output | c = any(DataFlow::FieldContent ct | ct.getName() = output.replaceAll("output\\.", "")) and - r.getInScopeEnvVarExpr(varName) = pred.asExpr() and + run.getInScopeEnvVarExpr(varName) = pred.asExpr() and exists(string script, string line | - script = r.getScript() and + script = run.getScript() and line = script.splitAt("\n") and ( output = line.regexpCapture(".*::set-output\\s+name=(.*)::.*", 1) or @@ -46,6 +46,6 @@ predicate runEnvToScriptStoreStep(DataFlow::Node pred, DataFlow::Node succ, Data ) and line.indexOf("$" + ["", "{", "ENV{"] + varName) > 0 ) and - succ.asExpr() = r + succ.asExpr() = run ) } diff --git a/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll b/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll index 11b8bf94bca..b5123069f13 100644 --- a/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll +++ b/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll @@ -232,8 +232,24 @@ predicate envCtxLocalStep(Node nodeFrom, Node nodeTo) { astFrom = nodeFrom.asExpr() and astTo = nodeTo.asExpr() and ( - externallyDefinedSource(nodeFrom, _, "env." + astTo.getFieldName()) or + externallyDefinedSource(nodeFrom, _, "env." + astTo.getFieldName()) + or astTo.getTarget() = astFrom + or + // e.g: + // - run: echo ISSUE_KEY=$(echo "${{ github.event.pull_request.title }}") >> $GITHUB_ENV + // - run: echo ${{ env.ISSUE_KEY }} + exists(Run run, string script, Expression expr, string line, string key, string value | + run.getScript() = script and + run.getAnScriptExpr() = expr and + line = script.splitAt("\n") and + key = line.regexpCapture("echo\\s+([^=]+)\\s*=(.*)>>\\s*\\$GITHUB_ENV", 1) and + value = line.regexpCapture("echo\\s+([^=]+)\\s*=(.*)>>\\s*\\$GITHUB_ENV", 2) and + value.indexOf(expr.getRawExpression()) > 0 and + key = astTo.getFieldName() and + expr = astFrom and + expr.getEnclosingWorkflow() = run.getEnclosingWorkflow() + ) ) ) } @@ -312,7 +328,7 @@ predicate fieldStoreStep(Node node1, Node node2, ContentSet c) { predicate storeStep(Node node1, ContentSet c, Node node2) { fieldStoreStep(node1, node2, c) or externallyDefinedStoreStep(node1, node2, c) or - runEnvToScriptStoreStep(node1, node2, c) + envToOutputStoreStep(node1, node2, c) } /** diff --git a/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll b/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll new file mode 100644 index 00000000000..abf36fd7da3 --- /dev/null +++ b/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll @@ -0,0 +1,50 @@ +import actions + +class ArtifactDownloadStep extends Step { + ArtifactDownloadStep() { + // eg: - uses: dawidd6/action-download-artifact@v2 + this.(UsesStep).getCallee() = "dawidd6/action-download-artifact" and + // exclude downloads outside the current directory + // TODO: add more checks to make sure the artifacts can be controlled + not exists(this.(UsesStep).getArgumentExpr("path")) + or + // eg: + // - uses: actions/github-script@v6 + // with: + // script: | + // let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + // owner: context.repo.owner, + // repo: context.repo.repo, + // run_id: context.payload.workflow_run.id, + // }); + // let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + // return artifact.name == "" + // })[0]; + // let download = await github.rest.actions.downloadArtifact({ + // owner: context.repo.owner, + // repo: context.repo.repo, + // artifact_id: matchArtifact.id, + // archive_format: 'zip', + // }); + this.(UsesStep).getCallee() = "actions/github-script" and + exists(string script | + this.(UsesStep).getArgument("script") = script and + script.matches("%listWorkflowRunArtifacts(%") and + script.matches("%downloadArtifact(%") + ) + or + // eg: - run: gh run download "${WORKFLOW_RUN_ID}" --repo "${GITHUB_REPOSITORY}" --name "artifact_name" + this.(Run).getScript().splitAt("\n").regexpMatch(".*gh\\s+run\\s+download.*") + or + // eg: + // run: | + // artifacts_url=${{ github.event.workflow_run.artifacts_url }} + // gh api "$artifacts_url" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact + // do + // IFS=$'\t' read name url <<< "$artifact" + // gh api $url > "$name.zip" + // unzip -d "$name" "$name.zip" + // done + this.(Run).getScript().splitAt("\n").matches("%github.event.workflow_run.artifacts_url%") + } +} diff --git a/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll b/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll index dbae3f48f80..330920852c1 100644 --- a/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll +++ b/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll @@ -4,12 +4,13 @@ private import codeql.actions.dataflow.ExternalFlow import codeql.actions.dataflow.FlowSources import codeql.actions.DataFlow -predicate writeToGithubEnvSink(DataFlow::Node sink) { - exists(Expression expr, Run run, string script, string line, string value | +predicate writeToGithubEnvSink(DataFlow::Node exprNode, string key, string value) { + exists(Expression expr, Run run, string script, string line | script = run.getScript() and line = script.splitAt("\n") and - value = line.regexpCapture("echo\\s+.*\\s*=(.*)>>\\s*\\$GITHUB_ENV", 1) and - expr = sink.asExpr() and + key = line.regexpCapture("echo\\s+([^=]+)\\s*=(.*)>>\\s*\\$GITHUB_ENV", 1) and + value = line.regexpCapture("echo\\s+([^=]+)\\s*=(.*)>>\\s*\\$GITHUB_ENV", 2) and + expr = exprNode.asExpr() and run.getAnScriptExpr() = expr and value.indexOf(expr.getRawExpression()) > 0 ) @@ -17,7 +18,7 @@ predicate writeToGithubEnvSink(DataFlow::Node sink) { private class EnvVarInjectionSink extends DataFlow::Node { EnvVarInjectionSink() { - writeToGithubEnvSink(this) or + writeToGithubEnvSink(this, _, _) or externallyDefinedSink(this, "envvar-injection") } } diff --git a/ql/src/Security/CWE-829/ArtifactPoisoning.ql b/ql/src/Security/CWE-829/ArtifactPoisoning.ql new file mode 100644 index 00000000000..5b0c4fc4e69 --- /dev/null +++ b/ql/src/Security/CWE-829/ArtifactPoisoning.ql @@ -0,0 +1,26 @@ +/** + * @name Artifact poisoning + * @description An attacker may be able to poison the workflow's artifacts and influence on consequent steps. + * @kind problem + * @problem.severity warning + * @precision medium + * @security-severity 9.3 + * @id actions/artifact-poisoning + * @tags actions + * security + * external/cwe/cwe-829 + */ + +import actions +import codeql.actions.security.ArtifactPoisoningQuery + +from LocalJob job, ArtifactDownloadStep download, Step run +where + job.getWorkflow().getATriggerEvent() = "workflow_run" and + (run instanceof Run or run instanceof UsesStep) and + exists(int i, int j | + job.getStep(i) = download and + job.getStep(j) = run and + i < j + ) +select download, "Potential artifact poisoning." diff --git a/ql/src/Security/CWE-829/UntrustedCheckout.ql b/ql/src/Security/CWE-829/UntrustedCheckout.ql index b33c7325526..86b80c67215 100644 --- a/ql/src/Security/CWE-829/UntrustedCheckout.ql +++ b/ql/src/Security/CWE-829/UntrustedCheckout.ql @@ -5,7 +5,7 @@ * that is able to push to the base repository and to access secrets. * @kind problem * @problem.severity warning - * @precision low + * @precision medium * @security-severity 9.3 * @id actions/untrusted-checkout * @tags actions diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/test1.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/test1.yml index b2780d54c04..3cab86f3171 100644 --- a/ql/test/query-tests/Security/CWE-077/.github/workflows/test1.yml +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/test1.yml @@ -21,5 +21,7 @@ jobs: - name: Extract Jira Key run: echo ISSUE_KEY=$(echo "${{ github.event.pull_request.title }}") >> $GITHUB_ENV + - name: Sink + run: echo ${{ env.ISSUE_KEY }} diff --git a/ql/test/query-tests/Security/CWE-077/EnvVarInjection.expected b/ql/test/query-tests/Security/CWE-077/EnvVarInjection.expected new file mode 100644 index 00000000000..2d96ec5a435 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/EnvVarInjection.expected @@ -0,0 +1,6 @@ +edges +nodes +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title | +subpaths +#select +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | ${{ github.event.pull_request.title }} | diff --git a/ql/test/query-tests/Security/CWE-077/PrivilegedEnvVarInjection.expected b/ql/test/query-tests/Security/CWE-077/PrivilegedEnvVarInjection.expected new file mode 100644 index 00000000000..2692d03eefe --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/PrivilegedEnvVarInjection.expected @@ -0,0 +1,6 @@ +edges +nodes +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title | +subpaths +#select +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | Potential privileged environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | ${{ github.event.pull_request.title }} | diff --git a/ql/test/query-tests/Security/CWE-094/.github/workflows/inter-job0.yml b/ql/test/query-tests/Security/CWE-094/.github/workflows/inter-job0.yml index d656fb65ea5..1ad46b0f6eb 100644 --- a/ql/test/query-tests/Security/CWE-094/.github/workflows/inter-job0.yml +++ b/ql/test/query-tests/Security/CWE-094/.github/workflows/inter-job0.yml @@ -36,7 +36,7 @@ jobs: if: ${{ always() }} - needs: job + needs: job1 steps: - id: sink diff --git a/ql/test/query-tests/Security/CWE-094/.github/workflows/test1.yml b/ql/test/query-tests/Security/CWE-094/.github/workflows/test1.yml new file mode 100644 index 00000000000..3cab86f3171 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-094/.github/workflows/test1.yml @@ -0,0 +1,27 @@ +name: Pull Request Open + +on: + pull_request_target: + branches: + - main + - 14.0.x + + types: + - opened + - reopened + +jobs: + updateJira: + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract Jira Key + run: echo ISSUE_KEY=$(echo "${{ github.event.pull_request.title }}") >> $GITHUB_ENV + + - name: Sink + run: echo ${{ env.ISSUE_KEY }} + + diff --git a/ql/test/query-tests/Security/CWE-094/CodeInjection.expected b/ql/test/query-tests/Security/CWE-094/CodeInjection.expected index 2ad85054803..1fad288860e 100644 --- a/ql/test/query-tests/Security/CWE-094/CodeInjection.expected +++ b/ql/test/query-tests/Security/CWE-094/CodeInjection.expected @@ -50,6 +50,7 @@ edges | .github/workflows/simple2.yml:14:9:18:6 | Uses Step: source | .github/workflows/simple2.yml:22:20:22:64 | steps.source.outputs.all_changed_files | | .github/workflows/simple2.yml:18:9:26:6 | Uses Step: step [value] | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | | .github/workflows/simple2.yml:22:20:22:64 | steps.source.outputs.all_changed_files | .github/workflows/simple2.yml:18:9:26:6 | Uses Step: step [value] | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | | .github/workflows/test.yml:8:7:10:4 | Job outputs node [job_output] | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | | .github/workflows/test.yml:8:20:8:50 | steps.step2.outputs.test | .github/workflows/test.yml:8:7:10:4 | Job outputs node [job_output] | | .github/workflows/test.yml:12:9:18:6 | Uses Step: step0 [value] | .github/workflows/test.yml:20:18:20:48 | steps.step0.outputs.value | @@ -185,6 +186,9 @@ nodes | .github/workflows/simple2.yml:18:9:26:6 | Uses Step: step [value] | semmle.label | Uses Step: step [value] | | .github/workflows/simple2.yml:22:20:22:64 | steps.source.outputs.all_changed_files | semmle.label | steps.source.outputs.all_changed_files | | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | semmle.label | steps.step.outputs.value | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title | +| .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | semmle.label | env.ISSUE_KEY | | .github/workflows/test.yml:8:7:10:4 | Job outputs node [job_output] | semmle.label | Job outputs node [job_output] | | .github/workflows/test.yml:8:20:8:50 | steps.step2.outputs.test | semmle.label | steps.step2.outputs.test | | .github/workflows/test.yml:12:9:18:6 | Uses Step: step0 [value] | semmle.label | Uses Step: step0 [value] | @@ -282,6 +286,8 @@ subpaths | .github/workflows/self_needs.yml:20:15:20:51 | needs.test1.outputs.job_output | .github/workflows/self_needs.yml:16:20:16:64 | github.event['head_commit']['message'] | .github/workflows/self_needs.yml:20:15:20:51 | needs.test1.outputs.job_output | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/self_needs.yml:20:15:20:51 | needs.test1.outputs.job_output | ${{ needs.test1.outputs.job_output }} | | .github/workflows/simple1.yml:16:18:16:49 | steps.summary.outputs.value | .github/workflows/simple1.yml:11:20:11:58 | github.event.head_commit.message | .github/workflows/simple1.yml:16:18:16:49 | steps.summary.outputs.value | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/simple1.yml:16:18:16:49 | steps.summary.outputs.value | ${{steps.summary.outputs.value}} | | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | .github/workflows/simple2.yml:14:9:18:6 | Uses Step: source | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | ${{ steps.step.outputs.value }} | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | ${{ github.event.pull_request.title }} | +| .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | ${{ env.ISSUE_KEY }} | | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | .github/workflows/test.yml:15:20:15:64 | github.event['head_commit']['message'] | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | ${{needs.job1.outputs['job_output']}} | | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | ${{ github.event.workflow_run.display_title }} | | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | Potential code injection in $@, which may be controlled by an external user. | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | ${{ github.event.workflow_run.head_commit.message }} | diff --git a/ql/test/query-tests/Security/CWE-094/PrivilegedCodeInjection.expected b/ql/test/query-tests/Security/CWE-094/PrivilegedCodeInjection.expected index 7061f509b81..25441104064 100644 --- a/ql/test/query-tests/Security/CWE-094/PrivilegedCodeInjection.expected +++ b/ql/test/query-tests/Security/CWE-094/PrivilegedCodeInjection.expected @@ -50,6 +50,7 @@ edges | .github/workflows/simple2.yml:14:9:18:6 | Uses Step: source | .github/workflows/simple2.yml:22:20:22:64 | steps.source.outputs.all_changed_files | | .github/workflows/simple2.yml:18:9:26:6 | Uses Step: step [value] | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | | .github/workflows/simple2.yml:22:20:22:64 | steps.source.outputs.all_changed_files | .github/workflows/simple2.yml:18:9:26:6 | Uses Step: step [value] | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | | .github/workflows/test.yml:8:7:10:4 | Job outputs node [job_output] | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | | .github/workflows/test.yml:8:20:8:50 | steps.step2.outputs.test | .github/workflows/test.yml:8:7:10:4 | Job outputs node [job_output] | | .github/workflows/test.yml:12:9:18:6 | Uses Step: step0 [value] | .github/workflows/test.yml:20:18:20:48 | steps.step0.outputs.value | @@ -185,6 +186,9 @@ nodes | .github/workflows/simple2.yml:18:9:26:6 | Uses Step: step [value] | semmle.label | Uses Step: step [value] | | .github/workflows/simple2.yml:22:20:22:64 | steps.source.outputs.all_changed_files | semmle.label | steps.source.outputs.all_changed_files | | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | semmle.label | steps.step.outputs.value | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title | +| .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | semmle.label | env.ISSUE_KEY | | .github/workflows/test.yml:8:7:10:4 | Job outputs node [job_output] | semmle.label | Job outputs node [job_output] | | .github/workflows/test.yml:8:20:8:50 | steps.step2.outputs.test | semmle.label | steps.step2.outputs.test | | .github/workflows/test.yml:12:9:18:6 | Uses Step: step0 [value] | semmle.label | Uses Step: step0 [value] | @@ -281,6 +285,8 @@ subpaths | .github/workflows/self_needs.yml:20:15:20:51 | needs.test1.outputs.job_output | .github/workflows/self_needs.yml:16:20:16:64 | github.event['head_commit']['message'] | .github/workflows/self_needs.yml:20:15:20:51 | needs.test1.outputs.job_output | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/self_needs.yml:20:15:20:51 | needs.test1.outputs.job_output | ${{ needs.test1.outputs.job_output }} | | .github/workflows/simple1.yml:16:18:16:49 | steps.summary.outputs.value | .github/workflows/simple1.yml:11:20:11:58 | github.event.head_commit.message | .github/workflows/simple1.yml:16:18:16:49 | steps.summary.outputs.value | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/simple1.yml:16:18:16:49 | steps.summary.outputs.value | ${{steps.summary.outputs.value}} | | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | .github/workflows/simple2.yml:14:9:18:6 | Uses Step: source | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/simple2.yml:29:24:29:54 | steps.step.outputs.value | ${{ steps.step.outputs.value }} | +| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | ${{ github.event.pull_request.title }} | +| .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:25:20:25:39 | env.ISSUE_KEY | ${{ env.ISSUE_KEY }} | | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | .github/workflows/test.yml:15:20:15:64 | github.event['head_commit']['message'] | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/test.yml:37:20:37:56 | needs.job1.outputs['job_output'] | ${{needs.job1.outputs['job_output']}} | | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/workflow_run.yml:9:19:9:64 | github.event.workflow_run.display_title | ${{ github.event.workflow_run.display_title }} | | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | Potential privileged code injection in $@, which may be controlled by an external user. | .github/workflows/workflow_run.yml:10:19:10:70 | github.event.workflow_run.head_commit.message | ${{ github.event.workflow_run.head_commit.message }} | diff --git a/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning1.yml b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning1.yml new file mode 100644 index 00000000000..4755350f0fc --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning1.yml @@ -0,0 +1,34 @@ +name: Pull Request Open + +on: + workflow_run: + workflows: ["Prev"] + types: + - completed + +jobs: + Download: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + - name: Run command + run: cmd + + + diff --git a/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning2.yml b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning2.yml new file mode 100644 index 00000000000..725038ab816 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning2.yml @@ -0,0 +1,21 @@ +name: Pull Request Open + +on: + workflow_run: + workflows: ["Prev"] + types: + - completed + +jobs: + Download: + runs-on: ubuntu-latest + steps: + - uses: dawidd6/action-download-artifact@v2 + with: + name: artifact_name + workflow: wf.yml + - name: Run command + run: cmd + + + diff --git a/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning3.yml b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning3.yml new file mode 100644 index 00000000000..4d2a9774753 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning3.yml @@ -0,0 +1,19 @@ +name: Pull Request Open + +on: + workflow_run: + workflows: ["Prev"] + types: + - completed + +jobs: + Download: + runs-on: ubuntu-latest + steps: + - run: | + gh run download "${WORKFLOW_RUN_ID}" --repo "${GITHUB_REPOSITORY}" --name "artifact_name" + - name: Run command + run: cmd + + + diff --git a/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning4.yml b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning4.yml new file mode 100644 index 00000000000..26d342f7060 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/.github/workflows/artifactpoisoning4.yml @@ -0,0 +1,25 @@ +name: Pull Request Open + +on: + workflow_run: + workflows: ["Prev"] + types: + - completed + +jobs: + Download: + runs-on: ubuntu-latest + steps: + - run: | + artifacts_url=${{ github.event.workflow_run.artifacts_url }} + gh api "$artifacts_url" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact + do + IFS=$'\t' read name url <<< "$artifact" + gh api $url > "$name.zip" + unzip -d "$name" "$name.zip" + done + - name: Run command + run: cmd + + + diff --git a/ql/test/query-tests/Security/CWE-829/.github/workflows/test1.yml b/ql/test/query-tests/Security/CWE-829/.github/workflows/test1.yml new file mode 100644 index 00000000000..3cab86f3171 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/.github/workflows/test1.yml @@ -0,0 +1,27 @@ +name: Pull Request Open + +on: + pull_request_target: + branches: + - main + - 14.0.x + + types: + - opened + - reopened + +jobs: + updateJira: + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract Jira Key + run: echo ISSUE_KEY=$(echo "${{ github.event.pull_request.title }}") >> $GITHUB_ENV + + - name: Sink + run: echo ${{ env.ISSUE_KEY }} + + diff --git a/ql/test/query-tests/Security/CWE-829/ArtifactPoisoning.expected b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoning.expected new file mode 100644 index 00000000000..8113215481c --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoning.expected @@ -0,0 +1,4 @@ +| .github/workflows/artifactpoisoning1.yml:13:9:30:6 | Uses Step | Potential artifact poisoning. | +| .github/workflows/artifactpoisoning2.yml:13:9:17:6 | Uses Step | Potential artifact poisoning. | +| .github/workflows/artifactpoisoning3.yml:13:9:15:6 | Run Step | Potential artifact poisoning. | +| .github/workflows/artifactpoisoning4.yml:13:9:21:6 | Run Step | Potential artifact poisoning. | diff --git a/ql/test/query-tests/Security/CWE-829/ArtifactPoisoning.qlref b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoning.qlref new file mode 100644 index 00000000000..21d37e957a1 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoning.qlref @@ -0,0 +1,2 @@ +Security/CWE-829/ArtifactPoisoning.ql + diff --git a/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected b/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected index c3a3ec2f988..5a572edf423 100644 --- a/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected +++ b/ql/test/query-tests/Security/CWE-829/UnpinnedActionsTag.expected @@ -1,5 +1,6 @@ | .github/workflows/actor_trusted_checkout.yml:19:7:23:4 | Uses Step | Unpinned 3rd party Action 'actor_trusted_checkout.yml' step $@ uses 'completely/fakeaction' with ref 'v2', not a pinned commit hash | .github/workflows/actor_trusted_checkout.yml:19:7:23:4 | Uses Step | Uses Step | | .github/workflows/actor_trusted_checkout.yml:23:7:26:21 | Uses Step | Unpinned 3rd party Action 'actor_trusted_checkout.yml' step $@ uses 'fakerepo/comment-on-pr' with ref 'v1', not a pinned commit hash | .github/workflows/actor_trusted_checkout.yml:23:7:26:21 | Uses Step | Uses Step | +| .github/workflows/artifactpoisoning2.yml:13:9:17:6 | Uses Step | Unpinned 3rd party Action 'Pull Request Open' step $@ uses 'dawidd6/action-download-artifact' with ref 'v2', not a pinned commit hash | .github/workflows/artifactpoisoning2.yml:13:9:17:6 | Uses Step | Uses Step | | .github/workflows/auto_ci.yml:93:9:96:6 | Uses Step | 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 | | .github/workflows/auto_ci.yml:108:9:119:6 | Uses Step: create_pr | 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 | | .github/workflows/auto_ci.yml:125:9:133:6 | Uses Step | 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 |