diff --git a/ql/lib/codeql/actions/Helper.qll b/ql/lib/codeql/actions/Helper.qll index 2953817de6b..1d88f6f6511 100644 --- a/ql/lib/codeql/actions/Helper.qll +++ b/ql/lib/codeql/actions/Helper.qll @@ -20,7 +20,7 @@ string wrapJsonRegexp(string regex) { } bindingset[str] -private string trimQuotes(string str) { +string trimQuotes(string str) { result = str.trim().regexpReplaceAll("^(\"|')", "").regexpReplaceAll("(\"|')$", "") } @@ -279,6 +279,10 @@ predicate inNonPrivilegedContext(AstNode node) { inNonPrivilegedJob(node) } +string partialFileContentRegexp() { + result = ["cat\\s+", "jq\\s+", "yq\\s+", "tail\\s+", "head\\s+", "ls\\s+"] +} + bindingset[snippet] predicate outputsPartialFileContent(string snippet) { // e.g. @@ -286,12 +290,7 @@ predicate outputsPartialFileContent(string snippet) { // echo "FOO=$(> $GITHUB_ENV // yq '.foo' foo.yml >> $GITHUB_PATH // cat foo.txt >> $GITHUB_PATH - snippet - .regexpMatch([ - "(\\$\\(|`)<.*", - ".*(\\b|^|\\s+)" + ["cat\\s+", "jq\\s+", "yq\\s+", "tail\\s+", "head\\s+", "ls\\s+"] + - ".*" - ]) + snippet.regexpMatch(["(\\$\\(|`)<.*", ".*(\\b|^|\\s+)" + partialFileContentRegexp() + ".*"]) } string defaultBranchNames() { diff --git a/ql/lib/codeql/actions/dataflow/FlowSteps.qll b/ql/lib/codeql/actions/dataflow/FlowSteps.qll index 5d0d45c26c1..aa31954ad3c 100644 --- a/ql/lib/codeql/actions/dataflow/FlowSteps.qll +++ b/ql/lib/codeql/actions/dataflow/FlowSteps.qll @@ -8,6 +8,7 @@ private import codeql.actions.DataFlow private import codeql.actions.dataflow.FlowSources private import codeql.actions.dataflow.ExternalFlow private import codeql.actions.security.ArtifactPoisoningQuery +private import codeql.actions.security.OutputClobberingQuery private import codeql.actions.security.UntrustedCheckoutQuery /** @@ -114,7 +115,8 @@ predicate envToRunStep(DataFlow::Node pred, DataFlow::Node succ) { succ.asExpr() = run.getScriptScalar() and ( envToSpecialFile(["GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_PATH"], var_name, run, _) or - envToArgInjSink(var_name, run, _) + envToArgInjSink(var_name, run, _) or + exists(OutputClobberingSink n | n.asExpr() = run.getScriptScalar()) ) ) } diff --git a/ql/lib/codeql/actions/security/OutputClobberingQuery.qll b/ql/lib/codeql/actions/security/OutputClobberingQuery.qll index af8f7af089d..5a85c22bb8f 100644 --- a/ql/lib/codeql/actions/security/OutputClobberingQuery.qll +++ b/ql/lib/codeql/actions/security/OutputClobberingQuery.qll @@ -92,6 +92,69 @@ class OutputClobberingFromEnvVarSink extends OutputClobberingSink { } } +/** + * - id: clob1 + * env: + * BODY: ${{ github.event.comment.body }} + * run: | + * # VULNERABLE + * echo $BODY + * echo "::set-output name=OUTPUT::SAFE" + * - id: clob2 + * env: + * BODY: ${{ github.event.comment.body }} + * run: | + * # VULNERABLE + * echo "::set-output name=OUTPUT::SAFE" + * echo $BODY + */ +class WorkflowCommandClobberingFromEnvVarSink extends OutputClobberingSink { + WorkflowCommandClobberingFromEnvVarSink() { + exists(Run run, string output_line, string clobbering_line, string var_name | + run.getScript().splitAt("\n") = output_line and + singleLineWorkflowCmd(output_line, "set-output", _, _) and + run.getScript().splitAt("\n") = clobbering_line and + clobbering_line.regexpMatch(".*echo\\s+(-e\\s+)?(\"|')?\\$(\\{)?" + var_name + ".*") and + exists(run.getInScopeEnvVarExpr(var_name)) and + run.getScriptScalar() = this.asExpr() + ) + } +} + +class WorkflowCommandClobberingFromFileReadSink extends OutputClobberingSink { + WorkflowCommandClobberingFromFileReadSink() { + exists(Run run, string output_line, string clobbering_line | + run.getScriptScalar() = this.asExpr() and + run.getScript().splitAt("\n") = output_line and + singleLineWorkflowCmd(output_line, "set-output", _, _) and + run.getScript().splitAt("\n") = clobbering_line and + ( + // A file is read and its content is assigned to an env var that gets printed to stdout + // - run: | + // foo=$(> $GITHUB_OUTPUT\necho "OUTPUT_2=$BODY" >> $GITHUB_OUTPUT\n | provenance | | | .github/workflows/output1.yml:30:9:35:6 | Uses Step | .github/workflows/output1.yml:36:14:38:58 | echo "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$(> $GITHUB_OUTPUT\n | provenance | | +| .github/workflows/output2.yml:9:18:9:49 | github.event.comment.body | .github/workflows/output2.yml:10:14:13:48 | # VULNERABLE\necho $BODY\necho "::set-output name=OUTPUT::SAFE"\n | provenance | | +| .github/workflows/output2.yml:16:18:16:49 | github.event.comment.body | .github/workflows/output2.yml:17:14:20:21 | # VULNERABLE\necho "::set-output name=OUTPUT::SAFE"\necho $BODY\n | provenance | | +| .github/workflows/output2.yml:36:9:41:6 | Uses Step | .github/workflows/output2.yml:42:14:46:48 | # VULNERABLE\nPR="$(> $GITHUB_OUTPUT\necho "OUTPUT_2=$BODY" >> $GITHUB_OUTPUT\n | semmle.label | # VULNERABLE\necho "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$BODY" >> $GITHUB_OUTPUT\n | | .github/workflows/output1.yml:30:9:35:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/output1.yml:36:14:38:58 | echo "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$(> $GITHUB_OUTPUT\n | semmle.label | echo "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$(> $GITHUB_OUTPUT\n | +| .github/workflows/output2.yml:9:18:9:49 | github.event.comment.body | semmle.label | github.event.comment.body | +| .github/workflows/output2.yml:10:14:13:48 | # VULNERABLE\necho $BODY\necho "::set-output name=OUTPUT::SAFE"\n | semmle.label | # VULNERABLE\necho $BODY\necho "::set-output name=OUTPUT::SAFE"\n | +| .github/workflows/output2.yml:16:18:16:49 | github.event.comment.body | semmle.label | github.event.comment.body | +| .github/workflows/output2.yml:17:14:20:21 | # VULNERABLE\necho "::set-output name=OUTPUT::SAFE"\necho $BODY\n | semmle.label | # VULNERABLE\necho "::set-output name=OUTPUT::SAFE"\necho $BODY\n | +| .github/workflows/output2.yml:36:9:41:6 | Uses Step | semmle.label | Uses Step | +| .github/workflows/output2.yml:42:14:46:48 | # VULNERABLE\nPR="$(> $GITHUB_OUTPUT\necho "OUTPUT_2=$BODY" >> $GITHUB_OUTPUT\n | .github/workflows/output1.yml:9:18:9:49 | github.event.comment.body | .github/workflows/output1.yml:10:14:13:50 | # VULNERABLE\necho "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$BODY" >> $GITHUB_OUTPUT\n | Potential clobbering of a step output in $@. | .github/workflows/output1.yml:10:14:13:50 | # VULNERABLE\necho "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$BODY" >> $GITHUB_OUTPUT\n | # VULNERABLE\necho "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$BODY" >> $GITHUB_OUTPUT\n | | .github/workflows/output1.yml:36:14:38:58 | echo "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$(> $GITHUB_OUTPUT\n | .github/workflows/output1.yml:30:9:35:6 | Uses Step | .github/workflows/output1.yml:36:14:38:58 | echo "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$(> $GITHUB_OUTPUT\n | Potential clobbering of a step output in $@. | .github/workflows/output1.yml:36:14:38:58 | echo "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$(> $GITHUB_OUTPUT\n | echo "OUTPUT_1=HARDCODED" >> $GITHUB_OUTPUT\necho "OUTPUT_2=$(> $GITHUB_OUTPUT\n | +| .github/workflows/output2.yml:10:14:13:48 | # VULNERABLE\necho $BODY\necho "::set-output name=OUTPUT::SAFE"\n | .github/workflows/output2.yml:9:18:9:49 | github.event.comment.body | .github/workflows/output2.yml:10:14:13:48 | # VULNERABLE\necho $BODY\necho "::set-output name=OUTPUT::SAFE"\n | Potential clobbering of a step output in $@. | .github/workflows/output2.yml:10:14:13:48 | # VULNERABLE\necho $BODY\necho "::set-output name=OUTPUT::SAFE"\n | # VULNERABLE\necho $BODY\necho "::set-output name=OUTPUT::SAFE"\n | +| .github/workflows/output2.yml:17:14:20:21 | # VULNERABLE\necho "::set-output name=OUTPUT::SAFE"\necho $BODY\n | .github/workflows/output2.yml:16:18:16:49 | github.event.comment.body | .github/workflows/output2.yml:17:14:20:21 | # VULNERABLE\necho "::set-output name=OUTPUT::SAFE"\necho $BODY\n | Potential clobbering of a step output in $@. | .github/workflows/output2.yml:17:14:20:21 | # VULNERABLE\necho "::set-output name=OUTPUT::SAFE"\necho $BODY\n | # VULNERABLE\necho "::set-output name=OUTPUT::SAFE"\necho $BODY\n | +| .github/workflows/output2.yml:42:14:46:48 | # VULNERABLE\nPR="$(