diff --git a/ql/lib/codeql/actions/Ast.qll b/ql/lib/codeql/actions/Ast.qll index cc29ceffe53..620f74e25bb 100644 --- a/ql/lib/codeql/actions/Ast.qll +++ b/ql/lib/codeql/actions/Ast.qll @@ -22,6 +22,10 @@ class AstNode instanceof AstNodeImpl { CompositeAction getEnclosingCompositeAction() { result = super.getEnclosingCompositeAction() } Expression getInScopeEnvVarExpr(string name) { result = super.getInScopeEnvVarExpr(name) } + + ScalarValue getInScopeDefaultValue(string name, string prop) { + result = super.getInScopeDefaultValue(name, prop) + } } class ScalarValue extends AstNode instanceof ScalarValueImpl { @@ -121,6 +125,10 @@ class ReusableWorkflow extends Workflow instanceof ReusableWorkflowImpl { class Input extends AstNode instanceof InputImpl { } +class Default extends AstNode instanceof DefaultsImpl { + ScalarValue getValue(string name, string prop) { result = super.getValue(name, prop) } +} + class Outputs extends AstNode instanceof OutputsImpl { Expression getAnOutputExpr() { result = super.getAnOutputExpr() } @@ -286,14 +294,18 @@ class ExternalJob extends Job, Uses instanceof ExternalJobImpl { } * See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun. */ class Run extends Step instanceof RunImpl { - string getScript() { result = super.getScript() } - - ScalarValue getScriptScalar() { result = super.getScriptScalar() } + ShellScript getScript() { result = super.getScript() } Expression getAnScriptExpr() { result = super.getAnScriptExpr() } string getWorkingDirectory() { result = super.getWorkingDirectory() } + string getShell() { result = super.getShell() } +} + +class ShellScript extends ScalarValueImpl instanceof ShellScriptImpl { + string getRawScript() { result = super.getRawScript() } + string getStmt(int i) { result = super.getStmt(i) } string getAStmt() { result = super.getAStmt() } @@ -302,19 +314,23 @@ class Run extends Step instanceof RunImpl { string getACommand() { result = super.getACommand() } - predicate getAssignment(int i, string name, string value) { super.getAssignment(i, name, value) } + string getFileReadCommand(int i) { result = super.getFileReadCommand(i) } - predicate getAnAssignment(string name, string value) { super.getAnAssignment(name, value) } + string getAFileReadCommand() { result = super.getAFileReadCommand() } - predicate getAWriteToGitHubEnv(string name, string value) { - super.getAWriteToGitHubEnv(name, value) + predicate getAssignment(int i, string name, string data) { super.getAssignment(i, name, data) } + + predicate getAnAssignment(string name, string data) { super.getAnAssignment(name, data) } + + predicate getAWriteToGitHubEnv(string name, string data) { + super.getAWriteToGitHubEnv(name, data) } - predicate getAWriteToGitHubOutput(string name, string value) { - super.getAWriteToGitHubOutput(name, value) + predicate getAWriteToGitHubOutput(string name, string data) { + super.getAWriteToGitHubOutput(name, data) } - predicate getAWriteToGitHubPath(string value) { super.getAWriteToGitHubPath(value) } + predicate getAWriteToGitHubPath(string data) { super.getAWriteToGitHubPath(data) } predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) { super.getAnEnvReachingGitHubOutputWrite(var, output_field) @@ -331,6 +347,18 @@ class Run extends Step instanceof RunImpl { predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) { super.getACmdReachingGitHubEnvWrite(cmd, output_field) } + + predicate getAnEnvReachingGitHubPathWrite(string var) { + super.getAnEnvReachingGitHubPathWrite(var) + } + + predicate getACmdReachingGitHubPathWrite(string cmd) { super.getACmdReachingGitHubPathWrite(cmd) } + + predicate fileToGitHubEnv(string path) { super.fileToGitHubEnv(path) } + + predicate fileToGitHubOutput(string path) { super.fileToGitHubOutput(path) } + + predicate fileToGitHubPath(string path) { super.fileToGitHubPath(path) } } abstract class SimpleReferenceExpression extends AstNode instanceof SimpleReferenceExpressionImpl { diff --git a/ql/lib/codeql/actions/Bash.qll b/ql/lib/codeql/actions/Bash.qll index fc9a75319eb..541ab437db2 100644 --- a/ql/lib/codeql/actions/Bash.qll +++ b/ql/lib/codeql/actions/Bash.qll @@ -1,7 +1,303 @@ private import codeql.actions.Ast -private import codeql.Locations -import codeql.actions.config.Config -private import codeql.actions.security.ControlChecks + +class BashShellScript extends ShellScript { + BashShellScript() { + exists(Run run | + this = run.getScript() and + run.getShell().matches("bash%") + ) + } + + private string lineProducer(int i) { + result = this.getRawScript().regexpReplaceAll("\\\\\\s*\n", "").splitAt("\n", i) + } + + private predicate cmdSubstitutionReplacement(string cmdSubs, string id, int k) { + exists(string line | line = this.lineProducer(k) | + exists(int i, int j | + cmdSubs = + // $() cmd substitution + line.regexpFind("\\$\\((?:[^()]+|\\((?:[^()]+|\\([^()]*\\))*\\))*\\)", i, j) + .regexpReplaceAll("^\\$\\(", "") + .regexpReplaceAll("\\)$", "") and + id = "cmdsubs:" + k + ":" + i + ":" + j + ) + or + exists(int i, int j | + // `...` cmd substitution + cmdSubs = + line.regexpFind("\\`[^\\`]+\\`", i, j) + .regexpReplaceAll("^\\`", "") + .regexpReplaceAll("\\`$", "") and + id = "cmd:" + k + ":" + i + ":" + j + ) + ) + } + + private predicate rankedCmdSubstitutionReplacements(int i, string old, string new) { + old = rank[i](string old2 | this.cmdSubstitutionReplacement(old2, _, _) | old2) and + this.cmdSubstitutionReplacement(old, new, _) + } + + private predicate doReplaceCmdSubstitutions(int line, int round, string old, string new) { + round = 0 and + old = this.lineProducer(line) and + new = old + or + round > 0 and + exists(string middle, string target, string replacement | + this.doReplaceCmdSubstitutions(line, round - 1, old, middle) and + this.rankedCmdSubstitutionReplacements(round, target, replacement) and + new = middle.replaceAll(target, replacement) + ) + } + + private string cmdSubstitutedLineProducer(int i) { + // script lines where any command substitution has been replaced with a unique placeholder + result = + max(int round, string new | + this.doReplaceCmdSubstitutions(i, round, _, new) + | + new order by round + ) + or + this.cmdSubstitutionReplacement(result, _, i) + } + + private predicate quotedStringReplacement(string quotedStr, string id) { + exists(string line, int k | line = this.cmdSubstitutedLineProducer(k) | + exists(int i, int j | + // double quoted string + quotedStr = line.regexpFind("\"((?:[^\"\\\\]|\\\\.)*)\"", i, j) and + id = + "qstr:" + k + ":" + i + ":" + j + ":" + quotedStr.length() + ":" + + quotedStr.regexpReplaceAll("[^a-zA-Z0-9]", "") + ) + or + exists(int i, int j | + // single quoted string + quotedStr = line.regexpFind("'((?:\\\\.|[^'\\\\])*)'", i, j) and + id = + "qstr:" + k + ":" + i + ":" + j + ":" + quotedStr.length() + ":" + + quotedStr.regexpReplaceAll("[^a-zA-Z0-9]", "") + ) + ) + } + + private predicate rankedQuotedStringReplacements(int i, string old, string new) { + old = rank[i](string old2 | this.quotedStringReplacement(old2, _) | old2) and + this.quotedStringReplacement(old, new) + } + + private predicate doReplaceQuotedStrings(int line, int round, string old, string new) { + round = 0 and + old = this.cmdSubstitutedLineProducer(line) and + new = old + or + round > 0 and + exists(string middle, string target, string replacement | + this.doReplaceQuotedStrings(line, round - 1, old, middle) and + this.rankedQuotedStringReplacements(round, target, replacement) and + new = middle.replaceAll(target, replacement) + ) + } + + private string quotedStringLineProducer(int i) { + result = + max(int round, string new | this.doReplaceQuotedStrings(i, round, _, new) | new order by round) + } + + private string stmtProducer(int i) { + result = this.quotedStringLineProducer(i).splitAt(Bash::splitSeparator()).trim() and + // when splitting the line with a separator that is not present, the result is the original line which may contain other separators + // we only one the split parts that do not contain any of the separators + not result.indexOf(Bash::splitSeparator()) > -1 + } + + private predicate doStmtRestoreQuotedStrings(int line, int round, string old, string new) { + round = 0 and + old = this.stmtProducer(line) and + new = old + or + round > 0 and + exists(string middle, string target, string replacement | + this.doStmtRestoreQuotedStrings(line, round - 1, old, middle) and + this.rankedQuotedStringReplacements(round, target, replacement) and + new = middle.replaceAll(replacement, target) + ) + } + + private string restoredStmtQuotedStringLineProducer(int i) { + result = + max(int round, string new | + this.doStmtRestoreQuotedStrings(i, round, _, new) + | + new order by round + ) + } + + private predicate doStmtRestoreCmdSubstitutions(int line, int round, string old, string new) { + round = 0 and + old = this.restoredStmtQuotedStringLineProducer(line) and + new = old + or + round > 0 and + exists(string middle, string target, string replacement | + this.doStmtRestoreCmdSubstitutions(line, round - 1, old, middle) and + this.rankedCmdSubstitutionReplacements(round, target, replacement) and + new = middle.replaceAll(replacement, target) + ) + } + + override string getStmt(int i) { + result = + max(int round, string new | + this.doStmtRestoreCmdSubstitutions(i, round, _, new) + | + new order by round + ) + } + + override string getAStmt() { result = this.getStmt(_) } + + private string cmdProducer(int i) { + result = this.quotedStringLineProducer(i).splitAt(Bash::separator()).trim() and + // when splitting the line with a separator that is not present, the result is the original line which may contain other separators + // we only one the split parts that do not contain any of the separators + not result.indexOf(Bash::separator()) > -1 + } + + private predicate doCmdRestoreQuotedStrings(int line, int round, string old, string new) { + round = 0 and + old = this.cmdProducer(line) and + new = old + or + round > 0 and + exists(string middle, string target, string replacement | + this.doCmdRestoreQuotedStrings(line, round - 1, old, middle) and + this.rankedQuotedStringReplacements(round, target, replacement) and + new = middle.replaceAll(replacement, target) + ) + } + + private string restoredCmdQuotedStringLineProducer(int i) { + result = + max(int round, string new | + this.doCmdRestoreQuotedStrings(i, round, _, new) + | + new order by round + ) + } + + private predicate doCmdRestoreCmdSubstitutions(int line, int round, string old, string new) { + round = 0 and + old = this.restoredCmdQuotedStringLineProducer(line) and + new = old + or + round > 0 and + exists(string middle, string target, string replacement | + this.doCmdRestoreCmdSubstitutions(line, round - 1, old, middle) and + this.rankedCmdSubstitutionReplacements(round, target, replacement) and + new = middle.replaceAll(replacement, target) + ) + } + + string getCmd(int i) { + result = + max(int round, string new | + this.doCmdRestoreCmdSubstitutions(i, round, _, new) + | + new order by round + ) + } + + string getACmd() { result = this.getCmd(_) } + + override string getCommand(int i) { + result = this.getCmd(i) and + // exclude variable declarations + not result.regexpMatch("^[a-zA-Z0-9\\-_]+=") and + // exclude the following keywords + not result = + [ + "", "for", "in", "do", "done", "if", "then", "else", "elif", "fi", "while", "until", "case", + "esac", "{", "}" + ] + } + + override string getACommand() { result = this.getCommand(_) } + + override string getFileReadCommand(int i) { + result = this.getStmt(i) and + result.matches(Bash::fileReadCommand() + "%") + } + + override string getAFileReadCommand() { result = this.getFileReadCommand(_) } + + override predicate getAssignment(int i, string name, string data) { + exists(string stmt | + stmt = this.getStmt(i) and + name = stmt.regexpCapture("^([a-zA-Z0-9\\-_]+)=.*", 1) and + data = stmt.regexpCapture("^[a-zA-Z0-9\\-_]+=(.*)", 1) + ) + } + + override predicate getAnAssignment(string name, string data) { this.getAssignment(_, name, data) } + + override predicate getAWriteToGitHubEnv(string name, string data) { + exists(string raw | + Bash::extractFileWrite(this.getRawScript(), "GITHUB_ENV", raw) and + Bash::extractVariableAndValue(raw, name, data) + ) + } + + override predicate getAWriteToGitHubOutput(string name, string data) { + exists(string raw | + Bash::extractFileWrite(this.getRawScript(), "GITHUB_OUTPUT", raw) and + Bash::extractVariableAndValue(raw, name, data) + ) + } + + override predicate getAWriteToGitHubPath(string data) { + Bash::extractFileWrite(this.getRawScript(), "GITHUB_PATH", data) + } + + override predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) { + Bash::envReachingGitHubFileWrite(this, var, "GITHUB_OUTPUT", output_field) + } + + override predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) { + Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_OUTPUT", output_field) + } + + override predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) { + Bash::envReachingGitHubFileWrite(this, var, "GITHUB_ENV", output_field) + } + + override predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) { + Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_ENV", output_field) + } + + override predicate getAnEnvReachingGitHubPathWrite(string var) { + Bash::envReachingGitHubFileWrite(this, var, "GITHUB_PATH", _) + } + + override predicate getACmdReachingGitHubPathWrite(string cmd) { + Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_PATH", _) + } + + override predicate fileToGitHubEnv(string path) { + Bash::fileToFileWrite(this, "GITHUB_ENV", path) + } + + override predicate fileToGitHubOutput(string path) { + Bash::fileToFileWrite(this, "GITHUB_OUTPUT", path) + } + + override predicate fileToGitHubPath(string path) { + Bash::fileToFileWrite(this, "GITHUB_PATH", path) + } +} module Bash { string stmtSeparator() { result = ";" } @@ -23,7 +319,7 @@ module Bash { result = pipeSeparator() } - string partialFileContentCommand() { result = ["cat", "jq", "yq", "tail", "head"] } + string fileReadCommand() { result = ["<", "cat", "jq", "yq", "tail", "head"] } /** Checks if expr is a bash command substitution */ bindingset[expr] @@ -133,8 +429,7 @@ module Bash { string script, string cmd, string file, string content, string filters ) { exists(string regexp | - regexp = - "(?i)(echo|printf|write-output)\\s*(.*?)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)" and + regexp = "(?i)(echo|printf)\\s*(.*?)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)" and cmd = script.regexpCapture(regexp, 1) and file = trimQuotes(script.regexpCapture(regexp, 5)) and filters = "" and @@ -145,13 +440,12 @@ module Bash { bindingset[script] predicate singleLineWorkflowCmd(string script, string cmd, string key, string value) { exists(string regexp | - regexp = - "(?i)(echo|printf|write-output)\\s*(['|\"])?::(set-[a-z]+)\\s*name\\s*=\\s*(.*?)::(.*)" and + regexp = "(?i)(echo|printf)\\s*(['|\"])?::(set-[a-z]+)\\s*name\\s*=\\s*(.*?)::(.*)" and cmd = script.regexpCapture(regexp, 3) and key = script.regexpCapture(regexp, 4) and value = trimQuotes(script.regexpCapture(regexp, 5)) or - regexp = "(?i)(echo|printf|write-output)\\s*(['|\"])?::(add-[a-z]+)\\s*::(.*)" and + regexp = "(?i)(echo|printf)\\s*(['|\"])?::(add-[a-z]+)\\s*::(.*)" and cmd = script.regexpCapture(regexp, 3) and key = "" and value = trimQuotes(script.regexpCapture(regexp, 4)) @@ -262,57 +556,38 @@ module Bash { } /** Writes the content of the file specified by `path` into a file pointed to by `file_var` */ - predicate fileToFileWrite(Run run, string file_var, string path) { + predicate fileToFileWrite(BashShellScript script, string file_var, string path) { exists(string regexp, string stmt, string file_expr | regexp = "(?i)(cat)\\s*" + "((?:(?!<<|<<-)[^>\n])+)\\s*" + "(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*" + "(\\S+)" and - stmt = run.getAStmt() and + stmt = script.getAStmt() and file_expr = trimQuotes(stmt.regexpCapture(regexp, 5)) and path = stmt.regexpCapture(regexp, 2) and containsParameterExpansion(file_expr, file_var, _, _) ) } - predicate fileToGitHubEnv(Run run, string path) { fileToFileWrite(run, "GITHUB_ENV", path) } - - predicate fileToGitHubOutput(Run run, string path) { fileToFileWrite(run, "GITHUB_OUTPUT", path) } - - predicate fileToGitHubPath(Run run, string path) { fileToFileWrite(run, "GITHUB_PATH", path) } - - bindingset[snippet] - predicate outputsPartialFileContent(Run run, string snippet) { - // e.g. - // echo FOO=`yq '.foo' foo.yml` >> $GITHUB_ENV - // echo "FOO=$(> $GITHUB_ENV - // yq '.foo' foo.yml >> $GITHUB_PATH - // cat foo.txt >> $GITHUB_PATH - exists(int i, string line, string cmd | - run.getStmt(i) = line and - line.indexOf(snippet.regexpReplaceAll("^\\$\\(", "").regexpReplaceAll("\\)$", "")) > -1 and - run.getCommand(i) = cmd and - cmd.indexOf(["<", Bash::partialFileContentCommand() + " "]) = 0 - ) - } - /** * Holds if the Run scripts contains an access to an environment variable called `var` * which value may get appended to the GITHUB_XXX special file */ - predicate envReachingGitHubFileWrite(Run run, string var, string file_var, string field) { + predicate envReachingGitHubFileWrite( + BashShellScript script, string var, string file_var, string field + ) { exists(string file_write_value | ( file_var = "GITHUB_ENV" and - run.getAWriteToGitHubEnv(field, file_write_value) + script.getAWriteToGitHubEnv(field, file_write_value) or file_var = "GITHUB_OUTPUT" and - run.getAWriteToGitHubOutput(field, file_write_value) + script.getAWriteToGitHubOutput(field, file_write_value) or file_var = "GITHUB_PATH" and field = "PATH" and - run.getAWriteToGitHubPath(file_write_value) + script.getAWriteToGitHubPath(file_write_value) ) and - envReachingRunExpr(run, var, file_write_value) + envReachingRunExpr(script, var, file_write_value) ) } @@ -321,11 +596,11 @@ module Bash { * Where the expression is a string captured from the Run's script. */ bindingset[expr] - predicate envReachingRunExpr(Run run, string var, string expr) { + predicate envReachingRunExpr(BashShellScript script, string var, string expr) { exists(string var2, string value2 | // VAR2=${VAR:-default} (var2=value2) // echo "FIELD=${VAR2:-default}" >> $GITHUB_ENV (field, file_write_value) - run.getAnAssignment(var2, value2) and + script.getAnAssignment(var2, value2) and containsParameterExpansion(value2, var, _, _) and containsParameterExpansion(expr, var2, _, _) ) @@ -339,33 +614,42 @@ module Bash { * Holds if the Run scripts contains a command substitution (`cmd`) * which output may get appended to the GITHUB_XXX special file */ - predicate cmdReachingGitHubFileWrite(Run run, string cmd, string file_var, string field) { + predicate cmdReachingGitHubFileWrite( + BashShellScript script, string cmd, string file_var, string field + ) { exists(string file_write_value | ( file_var = "GITHUB_ENV" and - run.getAWriteToGitHubEnv(field, file_write_value) + script.getAWriteToGitHubEnv(field, file_write_value) or file_var = "GITHUB_OUTPUT" and - run.getAWriteToGitHubOutput(field, file_write_value) + script.getAWriteToGitHubOutput(field, file_write_value) or file_var = "GITHUB_PATH" and field = "PATH" and - run.getAWriteToGitHubPath(file_write_value) + script.getAWriteToGitHubPath(file_write_value) ) and - ( - // cmd output is assigned to a second variable (var2) and var2 reaches the file write - exists(string var2, string value2 | - // VAR2=$(cmd) - // echo "FIELD=${VAR2:-default}" >> $GITHUB_ENV (field, file_write_value) - run.getAnAssignment(var2, value2) and - containsCmdSubstitution(value2, cmd) and - containsParameterExpansion(file_write_value, var2, _, _) - ) - or - // var reaches the file write directly - // echo "FIELD=$(cmd)" >> $GITHUB_ENV (field, file_write_value) - containsCmdSubstitution(file_write_value, cmd) - ) + cmdReachingRunExpr(script, cmd, file_write_value) ) } + + /** + * Holds if a command output is used, directly or indirectly, in a Run's step expression. + * Where the expression is a string captured from the Run's script. + */ + bindingset[expr] + predicate cmdReachingRunExpr(BashShellScript script, string cmd, string expr) { + // cmd output is assigned to a second variable (var2) and var2 reaches the file write + exists(string var2, string value2 | + // VAR2=$(cmd) + // echo "FIELD=${VAR2:-default}" >> $GITHUB_ENV (field, file_write_value) + script.getAnAssignment(var2, value2) and + containsCmdSubstitution(value2, cmd) and + containsParameterExpansion(expr, var2, _, _) + ) + or + // var reaches the file write directly + // echo "FIELD=$(cmd)" >> $GITHUB_ENV (field, file_write_value) + containsCmdSubstitution(expr, cmd) + } } diff --git a/ql/lib/codeql/actions/Helper.qll b/ql/lib/codeql/actions/Helper.qll index ae4405a185b..fb6fdf2d74b 100644 --- a/ql/lib/codeql/actions/Helper.qll +++ b/ql/lib/codeql/actions/Helper.qll @@ -3,6 +3,7 @@ private import codeql.Locations private import codeql.actions.security.ControlChecks import codeql.actions.config.Config import codeql.actions.Bash +import codeql.actions.PowerShell bindingset[expr] string normalizeExpr(string expr) { diff --git a/ql/lib/codeql/actions/PowerShell.qll b/ql/lib/codeql/actions/PowerShell.qll new file mode 100644 index 00000000000..1727930c2a3 --- /dev/null +++ b/ql/lib/codeql/actions/PowerShell.qll @@ -0,0 +1,50 @@ +private import codeql.actions.Ast + +class PowerShellScript extends ShellScript { + PowerShellScript() { + exists(Run run | + this = run.getScript() and + run.getShell().matches("pwsh%") + ) + } + + override string getStmt(int i) { none() } + + override string getAStmt() { none() } + + override string getCommand(int i) { none() } + + override string getACommand() { none() } + + override string getFileReadCommand(int i) { none() } + + override string getAFileReadCommand() { none() } + + override predicate getAssignment(int i, string name, string data) { none() } + + override predicate getAnAssignment(string name, string data) { none() } + + override predicate getAWriteToGitHubEnv(string name, string data) { none() } + + override predicate getAWriteToGitHubOutput(string name, string data) { none() } + + override predicate getAWriteToGitHubPath(string data) { none() } + + override predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) { none() } + + override predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) { none() } + + override predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) { none() } + + override predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) { none() } + + override predicate getAnEnvReachingGitHubPathWrite(string var) { none() } + + override predicate getACmdReachingGitHubPathWrite(string cmd) { none() } + + override predicate fileToGitHubEnv(string path) { none() } + + override predicate fileToGitHubOutput(string path) { none() } + + override predicate fileToGitHubPath(string path) { none() } +} diff --git a/ql/lib/codeql/actions/ast/internal/Ast.qll b/ql/lib/codeql/actions/ast/internal/Ast.qll index eaf1ae871a9..43772a978c5 100644 --- a/ql/lib/codeql/actions/ast/internal/Ast.qll +++ b/ql/lib/codeql/actions/ast/internal/Ast.qll @@ -62,6 +62,7 @@ private newtype TAstNode = n.lookup("jobs") instanceof YamlMapping } or TRunsNode(YamlMapping n) { exists(CompositeActionImpl a | a.getNode().lookup("runs") = n) } or + TDefaultsNode(YamlMapping n) { exists(YamlMapping m | m.lookup("defaults") = n) } or TInputsNode(YamlMapping n) { exists(YamlMapping m | m.lookup("inputs") = n) } or TInputNode(YamlValue n) { exists(YamlMapping m | m.lookup("inputs").(YamlMapping).maps(n, _)) } or TOutputsNode(YamlMapping n) { exists(YamlMapping m | m.lookup("outputs") = n) } or @@ -141,6 +142,19 @@ abstract class AstNodeImpl extends TAstNode { env.getParentNode().getAChildNode*() = this ) } + + ScalarValueImpl getInScopeDefaultValue(string name, string prop) { + exists(DefaultsImpl dft | + this.getEnclosingJob().getNode().(YamlMapping).maps(_, dft.getNode()) and + result = dft.getValue(name, prop) + ) + or + not exists(DefaultsImpl dft | this.getEnclosingJob() = dft.getParentNode()) and + exists(DefaultsImpl dft | + this.getEnclosingWorkflow().getNode().(YamlMapping).maps(_, dft.getNode()) and + result = dft.getValue(name, prop) + ) + } } class ScalarValueImpl extends AstNodeImpl, TScalarValueNode { @@ -165,6 +179,61 @@ class ScalarValueImpl extends AstNodeImpl, TScalarValueNode { string getValue() { result = value.getValue() } } +class ShellScriptImpl extends ScalarValueImpl { + ShellScriptImpl() { exists(YamlMapping run | run.lookup("run").(YamlScalar) = this.getNode()) } + + string getRawScript() { result = this.getValue().regexpReplaceAll("\\\\\\s*\n", "") } + + RunImpl getEnclosingRun() { result.getNode().lookup("run") = this.getNode() } + + abstract string getStmt(int i); + + abstract string getAStmt(); + + abstract string getCommand(int i); + + string getACommand() { + if this.getEnclosingRun().getShell().matches("bash%") + then result = this.(BashShellScript).getACommand() + else + if this.getEnclosingRun().getShell().matches("pwsh%") + then result = this.(PowerShellScript).getACommand() + else result = "NOT IMPLEMENTED" + } + + abstract string getFileReadCommand(int i); + + abstract string getAFileReadCommand(); + + abstract predicate getAssignment(int i, string name, string data); + + abstract predicate getAnAssignment(string name, string data); + + abstract predicate getAWriteToGitHubEnv(string name, string data); + + abstract predicate getAWriteToGitHubOutput(string name, string data); + + abstract predicate getAWriteToGitHubPath(string data); + + abstract predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field); + + abstract predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field); + + abstract predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field); + + abstract predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field); + + abstract predicate getAnEnvReachingGitHubPathWrite(string var); + + abstract predicate getACmdReachingGitHubPathWrite(string cmd); + + abstract predicate fileToGitHubEnv(string path); + + abstract predicate fileToGitHubOutput(string path); + + abstract predicate fileToGitHubPath(string path); +} + class ExpressionImpl extends AstNodeImpl, TExpressionNode { YamlNode key; YamlString value; @@ -493,6 +562,28 @@ class InputsImpl extends AstNodeImpl, TInputsNode { } } +class DefaultsImpl extends AstNodeImpl, TDefaultsNode { + YamlMapping n; + + DefaultsImpl() { this = TDefaultsNode(n) } + + override string toString() { result = n.toString() } + + override AstNodeImpl getAChildNode() { result.getNode() = n.getAChildNode*() } + + override AstNodeImpl getParentNode() { result.getAChildNode() = this } + + override string getAPrimaryQlClass() { result = "DefaultsImpl" } + + override Location getLocation() { result = n.getLocation() } + + override YamlMapping getNode() { result = n } + + ScalarValueImpl getValue(string name, string prop) { + n.lookup(name).(YamlMapping).lookup(prop) = result.getNode() + } +} + class InputImpl extends AstNodeImpl, TInputNode { YamlValue n; @@ -1314,20 +1405,18 @@ class ExternalJobImpl extends JobImpl, UsesImpl { class RunImpl extends StepImpl { YamlScalar script; + ScalarValueImpl scriptScalar; - RunImpl() { this.getNode().lookup("run") = script } - - string getScript() { result = script.getValue().regexpReplaceAll("\\\\\\s*\n", "") } - - ScalarValueImpl getScriptScalar() { result = TScalarValueNode(script) } - - ExpressionImpl getAnScriptExpr() { result.getParentNode().getNode() = script } + RunImpl() { + this.getNode().lookup("run") = script and + scriptScalar = TScalarValueNode(script) + } override string toString() { if exists(this.getId()) then result = "Run Step: " + this.getId() else result = "Run Step" } - /** Gets the working directory for this `runs` mapping. */ + /** Gets the working directory for this `run` mapping. */ string getWorkingDirectory() { if exists(n.lookup("working-directory").(YamlString).getValue()) then @@ -1339,268 +1428,19 @@ class RunImpl extends StepImpl { else result = "GITHUB_WORKSPACE/" } - private string lineProducer(int i) { - result = script.getValue().regexpReplaceAll("\\\\\\s*\n", "").splitAt("\n", i) + /** Gets the shell for this `run` mapping. */ + string getShell() { + if exists(n.lookup("shell").(YamlString).getValue()) + then result = n.lookup("shell").(YamlString).getValue() + else + if exists(this.getInScopeDefaultValue("run", "shell")) + then result = this.getInScopeDefaultValue("run", "shell").getValue() + else result = "bash" } - private predicate cmdSubstitutionReplacement(string cmdSubs, string id, int k) { - exists(string line | line = this.lineProducer(k) | - exists(int i, int j | - cmdSubs = - // $() cmd substitution - line.regexpFind("\\$\\((?:[^()]+|\\((?:[^()]+|\\([^()]*\\))*\\))*\\)", i, j) - .regexpReplaceAll("^\\$\\(", "") - .regexpReplaceAll("\\)$", "") and - id = "cmdsubs:" + k + ":" + i + ":" + j - ) - or - exists(int i, int j | - // `...` cmd substitution - cmdSubs = - line.regexpFind("\\`[^\\`]+\\`", i, j) - .regexpReplaceAll("^\\`", "") - .regexpReplaceAll("\\`$", "") and - id = "cmd:" + k + ":" + i + ":" + j - ) - ) - } + ShellScriptImpl getScript() { result = scriptScalar } - private predicate rankedCmdSubstitutionReplacements(int i, string old, string new) { - old = rank[i](string old2 | this.cmdSubstitutionReplacement(old2, _, _) | old2) and - this.cmdSubstitutionReplacement(old, new, _) - } - - private predicate doReplaceCmdSubstitutions(int line, int round, string old, string new) { - round = 0 and - old = this.lineProducer(line) and - new = old - or - round > 0 and - exists(string middle, string target, string replacement | - this.doReplaceCmdSubstitutions(line, round - 1, old, middle) and - this.rankedCmdSubstitutionReplacements(round, target, replacement) and - new = middle.replaceAll(target, replacement) - ) - } - - private string cmdSubstitutedLineProducer(int i) { - // script lines where any command substitution has been replaced with a unique placeholder - result = - max(int round, string new | - this.doReplaceCmdSubstitutions(i, round, _, new) - | - new order by round - ) - or - this.cmdSubstitutionReplacement(result, _, i) - } - - private predicate quotedStringReplacement(string quotedStr, string id) { - exists(string line, int k | line = this.cmdSubstitutedLineProducer(k) | - exists(int i, int j | - // double quoted string - quotedStr = line.regexpFind("\"((?:[^\"\\\\]|\\\\.)*)\"", i, j) and - id = - "qstr:" + k + ":" + i + ":" + j + ":" + quotedStr.length() + ":" + - quotedStr.regexpReplaceAll("[^a-zA-Z0-9]", "") - ) - or - exists(int i, int j | - // single quoted string - quotedStr = line.regexpFind("'((?:\\\\.|[^'\\\\])*)'", i, j) and - id = - "qstr:" + k + ":" + i + ":" + j + ":" + quotedStr.length() + ":" + - quotedStr.regexpReplaceAll("[^a-zA-Z0-9]", "") - ) - ) - } - - private predicate rankedQuotedStringReplacements(int i, string old, string new) { - old = rank[i](string old2 | this.quotedStringReplacement(old2, _) | old2) and - this.quotedStringReplacement(old, new) - } - - private predicate doReplaceQuotedStrings(int line, int round, string old, string new) { - round = 0 and - old = this.cmdSubstitutedLineProducer(line) and - new = old - or - round > 0 and - exists(string middle, string target, string replacement | - this.doReplaceQuotedStrings(line, round - 1, old, middle) and - this.rankedQuotedStringReplacements(round, target, replacement) and - new = middle.replaceAll(target, replacement) - ) - } - - private string quotedStringLineProducer(int i) { - result = - max(int round, string new | this.doReplaceQuotedStrings(i, round, _, new) | new order by round) - } - - private string stmtProducer(int i) { - result = this.quotedStringLineProducer(i).splitAt(Bash::splitSeparator()).trim() and - // when splitting the line with a separator that is not present, the result is the original line which may contain other separators - // we only one the split parts that do not contain any of the separators - not result.indexOf(Bash::splitSeparator()) > -1 - } - - private predicate doStmtRestoreQuotedStrings(int line, int round, string old, string new) { - round = 0 and - old = this.stmtProducer(line) and - new = old - or - round > 0 and - exists(string middle, string target, string replacement | - this.doStmtRestoreQuotedStrings(line, round - 1, old, middle) and - this.rankedQuotedStringReplacements(round, target, replacement) and - new = middle.replaceAll(replacement, target) - ) - } - - private string restoredStmtQuotedStringLineProducer(int i) { - result = - max(int round, string new | - this.doStmtRestoreQuotedStrings(i, round, _, new) - | - new order by round - ) - } - - private predicate doStmtRestoreCmdSubstitutions(int line, int round, string old, string new) { - round = 0 and - old = this.restoredStmtQuotedStringLineProducer(line) and - new = old - or - round > 0 and - exists(string middle, string target, string replacement | - this.doStmtRestoreCmdSubstitutions(line, round - 1, old, middle) and - this.rankedCmdSubstitutionReplacements(round, target, replacement) and - new = middle.replaceAll(replacement, target) - ) - } - - string getStmt(int i) { - result = - max(int round, string new | - this.doStmtRestoreCmdSubstitutions(i, round, _, new) - | - new order by round - ) - } - - string getAStmt() { result = this.getStmt(_) } - - private string cmdProducer(int i) { - result = this.quotedStringLineProducer(i).splitAt(Bash::separator()).trim() and - // when splitting the line with a separator that is not present, the result is the original line which may contain other separators - // we only one the split parts that do not contain any of the separators - not result.indexOf(Bash::separator()) > -1 - } - - private predicate doCmdRestoreQuotedStrings(int line, int round, string old, string new) { - round = 0 and - old = this.cmdProducer(line) and - new = old - or - round > 0 and - exists(string middle, string target, string replacement | - this.doCmdRestoreQuotedStrings(line, round - 1, old, middle) and - this.rankedQuotedStringReplacements(round, target, replacement) and - new = middle.replaceAll(replacement, target) - ) - } - - private string restoredCmdQuotedStringLineProducer(int i) { - result = - max(int round, string new | - this.doCmdRestoreQuotedStrings(i, round, _, new) - | - new order by round - ) - } - - private predicate doCmdRestoreCmdSubstitutions(int line, int round, string old, string new) { - round = 0 and - old = this.restoredCmdQuotedStringLineProducer(line) and - new = old - or - round > 0 and - exists(string middle, string target, string replacement | - this.doCmdRestoreCmdSubstitutions(line, round - 1, old, middle) and - this.rankedCmdSubstitutionReplacements(round, target, replacement) and - new = middle.replaceAll(replacement, target) - ) - } - - string getCmd(int i) { - result = - max(int round, string new | - this.doCmdRestoreCmdSubstitutions(i, round, _, new) - | - new order by round - ) - } - - string getACmd() { result = this.getCmd(_) } - - string getCommand(int i) { - result = this.getCmd(i) and - // exclude variable declarations - not result.regexpMatch("^[a-zA-Z0-9\\-_]+=") and - // exclude the following keywords - not result = - [ - "", "for", "in", "do", "done", "if", "then", "else", "elif", "fi", "while", "until", "case", - "esac", "{", "}" - ] - } - - string getACommand() { result = this.getCommand(_) } - - predicate getAssignment(int i, string name, string value) { - exists(string stmt | - stmt = this.getStmt(i) and - name = stmt.regexpCapture("^([a-zA-Z0-9\\-_]+)=.*", 1) and - value = stmt.regexpCapture("^[a-zA-Z0-9\\-_]+=(.*)", 1) - ) - } - - predicate getAnAssignment(string name, string value) { this.getAssignment(_, name, value) } - - predicate getAWriteToGitHubEnv(string name, string value) { - exists(string raw | - Bash::extractFileWrite(this.getScript(), "GITHUB_ENV", raw) and - Bash::extractVariableAndValue(raw, name, value) - ) - } - - predicate getAWriteToGitHubOutput(string name, string value) { - exists(string raw | - Bash::extractFileWrite(this.getScript(), "GITHUB_OUTPUT", raw) and - Bash::extractVariableAndValue(raw, name, value) - ) - } - - predicate getAWriteToGitHubPath(string value) { - Bash::extractFileWrite(this.getScript(), "GITHUB_PATH", value) - } - - predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) { - Bash::envReachingGitHubFileWrite(this, var, "GITHUB_OUTPUT", output_field) - } - - predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) { - Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_OUTPUT", output_field) - } - - predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) { - Bash::envReachingGitHubFileWrite(this, var, "GITHUB_ENV", output_field) - } - - predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) { - Bash::cmdReachingGitHubFileWrite(this, cmd, "GITHUB_ENV", output_field) - } + ExpressionImpl getAnScriptExpr() { result.getParentNode().getNode() = script } } /** diff --git a/ql/lib/codeql/actions/controlflow/internal/Cfg.qll b/ql/lib/codeql/actions/controlflow/internal/Cfg.qll index 8a6e52309fb..5ceab79820b 100644 --- a/ql/lib/codeql/actions/controlflow/internal/Cfg.qll +++ b/ql/lib/codeql/actions/controlflow/internal/Cfg.qll @@ -282,7 +282,7 @@ private class RunTree extends StandardPreOrderTree instanceof Run { ( child = super.getInScopeEnvVarExpr(_) or child = super.getAnScriptExpr() or - child = super.getScriptScalar() + child = super.getScript() ) and l = child.getLocation() | diff --git a/ql/lib/codeql/actions/dataflow/FlowSources.qll b/ql/lib/codeql/actions/dataflow/FlowSources.qll index f1fb2073ed0..b30fd5495ed 100644 --- a/ql/lib/codeql/actions/dataflow/FlowSources.qll +++ b/ql/lib/codeql/actions/dataflow/FlowSources.qll @@ -86,7 +86,8 @@ class GitCommandSource extends RemoteFlowSource, CommandSource { exists(Uses uses | checkout = uses and uses.getCallee() = "actions/checkout" and - exists(uses.getArgument("ref")) + exists(uses.getArgument("ref")) and + not uses.getArgument("ref").matches("%base%") ) or checkout instanceof GitMutableRefCheckout @@ -97,9 +98,9 @@ class GitCommandSource extends RemoteFlowSource, CommandSource { or checkout instanceof GhSHACheckout ) and - this.asExpr() = run.getScriptScalar() and + this.asExpr() = run.getScript() and checkout.getAFollowingStep() = run and - run.getACommand() = cmd and + run.getScript().getACommand() = cmd and cmd.indexOf("git") = 0 and untrustedGitCommandsDataModel(cmd_regex, flag) and cmd.regexpMatch(cmd_regex) @@ -127,8 +128,8 @@ class GitHubEventPathSource extends RemoteFlowSource, CommandSource { // PR_TITLE=$(jq --raw-output .pull_request.title ${GITHUB_EVENT_PATH}) // BODY=$(jq -r '.issue.body' "$GITHUB_EVENT_PATH" | sed -n '3p') GitHubEventPathSource() { - this.asExpr() = run.getScriptScalar() and - run.getACommand() = cmd and + this.asExpr() = run.getScript() and + run.getScript().getACommand() = cmd and cmd.matches("jq%") and cmd.matches("%GITHUB_EVENT_PATH%") and exists(string regexp | @@ -207,10 +208,11 @@ private class CheckoutSource extends RemoteFlowSource, FileSource { // but PRHeadCheckoutStep uses Taint Tracking anc causes a non-Monolitic Recursion error // so we list all the subclasses of PRHeadCheckoutStep here and use actions/checkout as a workaround // instead of using ActionsMutableRefCheckout and ActionsSHACheckout - exists(Uses u | - this.asExpr() = u and - u.getCallee() = "actions/checkout" and - exists(u.getArgument("ref")) + exists(Uses uses | + this.asExpr() = uses and + uses.getCallee() = "actions/checkout" and + exists(uses.getArgument("ref")) and + not uses.getArgument("ref").matches("%base%") ) or this.asExpr() instanceof GitMutableRefCheckout diff --git a/ql/lib/codeql/actions/dataflow/FlowSteps.qll b/ql/lib/codeql/actions/dataflow/FlowSteps.qll index 787a5f72084..0f7e906685b 100644 --- a/ql/lib/codeql/actions/dataflow/FlowSteps.qll +++ b/ql/lib/codeql/actions/dataflow/FlowSteps.qll @@ -23,7 +23,7 @@ predicate envToOutputStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlo exists(Run run, string var, string field | run.getInScopeEnvVarExpr(var) = pred.asExpr() and succ.asExpr() = run and - Bash::envReachingGitHubFileWrite(run, var, "GITHUB_OUTPUT", field) and + run.getScript().getAnEnvReachingGitHubOutputWrite(var, field) and c = any(DataFlow::FieldContent ct | ct.getName() = field) ) } @@ -35,8 +35,8 @@ predicate envToEnvStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow:: run.getInScopeEnvVarExpr(var) = pred.asExpr() and // we store the taint on the enclosing job since the may not exist an implicit env attribute succ.asExpr() = run.getEnclosingJob() and - Bash::envReachingGitHubFileWrite(run, var, "GITHUB_ENV", field) and - c = any(DataFlow::FieldContent ct | ct.getName() = field) //and + run.getScript().getAnEnvReachingGitHubEnvWrite(var, field) and + c = any(DataFlow::FieldContent ct | ct.getName() = field) ) } @@ -55,12 +55,12 @@ predicate commandToOutputStoreStep(DataFlow::Node pred, DataFlow::Node succ, Dat or exists(FileSource source | source.asExpr().(Step).getAFollowingStep() = run and - Bash::outputsPartialFileContent(run, cmd) + run.getScript().getAFileReadCommand() = cmd ) ) and - Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_OUTPUT", key) and + run.getScript().getACmdReachingGitHubOutputWrite(cmd, key) and c = any(DataFlow::FieldContent ct | ct.getName() = key) and - pred.asExpr() = run.getScriptScalar() and + pred.asExpr() = run.getScript() and succ.asExpr() = run ) } @@ -80,12 +80,12 @@ predicate commandToEnvStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFl or exists(FileSource source | source.asExpr().(Step).getAFollowingStep() = run and - Bash::outputsPartialFileContent(run, cmd) + run.getScript().getAFileReadCommand() = cmd ) ) and - Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_ENV", key) and + run.getScript().getACmdReachingGitHubEnvWrite(cmd, key) and c = any(DataFlow::FieldContent ct | ct.getName() = key) and - pred.asExpr() = run.getScriptScalar() and + pred.asExpr() = run.getScript() and // we store the taint on the enclosing job since there may not be an implicit env attribute succ.asExpr() = run.getEnclosingJob() ) diff --git a/ql/lib/codeql/actions/dataflow/TaintSteps.qll b/ql/lib/codeql/actions/dataflow/TaintSteps.qll index de64a0dd6f4..e9d5a44c929 100644 --- a/ql/lib/codeql/actions/dataflow/TaintSteps.qll +++ b/ql/lib/codeql/actions/dataflow/TaintSteps.qll @@ -22,14 +22,14 @@ class AdditionalTaintStep extends Unit { } /** - * A download artifact step followed by a step that may use downloaded artifacts. + * A file source step followed by a Run step may read the file. */ predicate fileDownloadToRunStep(DataFlow::Node pred, DataFlow::Node succ) { exists(FileSource source, Run run | pred = source and source.asExpr().(Step).getAFollowingStep() = run and - succ.asExpr() = run.getScriptScalar() and - Bash::outputsPartialFileContent(run, run.getACommand()) + succ.asExpr() = run.getScript() and + exists(run.getScript().getAFileReadCommand()) ) } diff --git a/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll b/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll index 18ff398ebab..a0309437292 100644 --- a/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll +++ b/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll @@ -17,11 +17,11 @@ abstract class ArgumentInjectionSink extends DataFlow::Node { bindingset[var] predicate envToArgInjSink(string var, Run run, string command) { exists(string argument, string cmd, string regexp, int command_group, int argument_group | - run.getACommand() = cmd and + run.getScript().getACommand() = cmd and argumentInjectionSinksDataModel(regexp, command_group, argument_group) and command = cmd.regexpCapture(regexp, command_group) and argument = cmd.regexpCapture(regexp, argument_group) and - Bash::envReachingRunExpr(run, var, argument) and + Bash::envReachingRunExpr(run.getScript(), var, argument) and exists(run.getInScopeEnvVarExpr(var)) ) } @@ -40,15 +40,15 @@ class ArgumentInjectionFromEnvVarSink extends ArgumentInjectionSink { ArgumentInjectionFromEnvVarSink() { exists(Run run, string var | envToArgInjSink(var, run, command) and - run.getScriptScalar() = this.asExpr() and + run.getScript() = this.asExpr() and exists(run.getInScopeEnvVarExpr(var)) ) or exists( Run run, string cmd, string argument, string regexp, int argument_group, int command_group | - run.getACommand() = cmd and - run.getScriptScalar() = this.asExpr() and + run.getScript().getACommand() = cmd and + run.getScript() = this.asExpr() and argumentInjectionSinksDataModel(regexp, command_group, argument_group) and argument = cmd.regexpCapture(regexp, argument_group) and command = cmd.regexpCapture(regexp, command_group) and @@ -75,8 +75,8 @@ class ArgumentInjectionFromCommandSink extends ArgumentInjectionSink { int command_group | run = source.getEnclosingRun() and - this.asExpr() = run.getScriptScalar() and - cmd = run.getACommand() and + this.asExpr() = run.getScript() and + cmd = run.getScript().getACommand() and argumentInjectionSinksDataModel(regexp, command_group, argument_group) and argument = cmd.regexpCapture(regexp, argument_group) and command = cmd.regexpCapture(regexp, command_group) @@ -106,8 +106,8 @@ private module ArgumentInjectionConfig implements DataFlow::ConfigSig { exists( Run run, string argument, string cmd, string regexp, int command_group, int argument_group | - run.getScriptScalar() = source.asExpr() and - run.getACommand() = cmd and + run.getScript() = source.asExpr() and + run.getScript().getACommand() = cmd and argumentInjectionSinksDataModel(regexp, command_group, argument_group) and argument = cmd.regexpCapture(regexp, argument_group) and argument.regexpMatch(".*\\$(\\{)?(GITHUB_HEAD_REF).*") @@ -119,7 +119,7 @@ private module ArgumentInjectionConfig implements DataFlow::ConfigSig { predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) { exists(Run run, string var | run.getInScopeEnvVarExpr(var) = pred.asExpr() and - succ.asExpr() = run.getScriptScalar() and + succ.asExpr() = run.getScript() and envToArgInjSink(var, run, _) ) } diff --git a/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll b/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll index 31a9edd03b3..d06b125ca32 100644 --- a/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll +++ b/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll @@ -155,15 +155,21 @@ class ActionsGitHubScriptDownloadStep extends UntrustedArtifactDownloadStep, Use } override string getPath() { - if this.getAFollowingStep().(Run).getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) + if + this.getAFollowingStep() + .(Run) + .getScript() + .getACommand() + .regexpMatch(unzipRegexp() + unzipDirArgRegexp()) then result = normalizePath(trimQuotes(this.getAFollowingStep() .(Run) + .getScript() .getACommand() .regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 2))) else - if this.getAFollowingStep().(Run).getACommand().regexpMatch(unzipRegexp()) + if this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp()) then result = "GITHUB_WORKSPACE/" else none() } @@ -172,31 +178,37 @@ class ActionsGitHubScriptDownloadStep extends UntrustedArtifactDownloadStep, Use class GHRunArtifactDownloadStep extends UntrustedArtifactDownloadStep, Run { GHRunArtifactDownloadStep() { // eg: - run: gh run download ${{ github.event.workflow_run.id }} --repo "${GITHUB_REPOSITORY}" --name "artifact_name" - this.getACommand().regexpMatch(".*gh\\s+run\\s+download.*") and - this.getACommand().matches("%github.event.workflow_run.id%") and + this.getScript().getACommand().regexpMatch(".*gh\\s+run\\s+download.*") and + this.getScript().getACommand().matches("%github.event.workflow_run.id%") and ( - this.getACommand().regexpMatch(unzipRegexp()) or - this.getAFollowingStep().(Run).getACommand().regexpMatch(unzipRegexp()) + this.getScript().getACommand().regexpMatch(unzipRegexp()) or + this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp()) ) } override string getPath() { if - this.getAFollowingStep().(Run).getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) or - this.getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) + this.getAFollowingStep() + .(Run) + .getScript() + .getACommand() + .regexpMatch(unzipRegexp() + unzipDirArgRegexp()) or + this.getScript().getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) then result = - normalizePath(trimQuotes(this.getACommand() + normalizePath(trimQuotes(this.getScript() + .getACommand() .regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 2))) or result = normalizePath(trimQuotes(this.getAFollowingStep() .(Run) + .getScript() .getACommand() .regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 2))) else if - this.getAFollowingStep().(Run).getACommand().regexpMatch(unzipRegexp()) or - this.getACommand().regexpMatch(unzipRegexp()) + this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp()) or + this.getScript().getACommand().regexpMatch(unzipRegexp()) then result = "GITHUB_WORKSPACE/" else none() } @@ -213,24 +225,30 @@ class DirectArtifactDownloadStep extends UntrustedArtifactDownloadStep, Run { // gh api $url > "$name.zip" // unzip -d "$name" "$name.zip" // done - this.getACommand().matches("%github.event.workflow_run.artifacts_url%") and + this.getScript().getACommand().matches("%github.event.workflow_run.artifacts_url%") and ( - this.getACommand().regexpMatch(unzipRegexp()) or - this.getAFollowingStep().(Run).getACommand().regexpMatch(unzipRegexp()) + this.getScript().getACommand().regexpMatch(unzipRegexp()) or + this.getAFollowingStep().(Run).getScript().getACommand().regexpMatch(unzipRegexp()) ) } override string getPath() { if - this.getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) or - this.getAFollowingStep().(Run).getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) + this.getScript().getACommand().regexpMatch(unzipRegexp() + unzipDirArgRegexp()) or + this.getAFollowingStep() + .(Run) + .getScript() + .getACommand() + .regexpMatch(unzipRegexp() + unzipDirArgRegexp()) then result = - normalizePath(trimQuotes(this.getACommand() + normalizePath(trimQuotes(this.getScript() + .getACommand() .regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 2))) or result = normalizePath(trimQuotes(this.getAFollowingStep() .(Run) + .getScript() .getACommand() .regexpCapture(unzipRegexp() + unzipDirArgRegexp(), 2))) else result = "GITHUB_WORKSPACE/" @@ -246,7 +264,7 @@ class ArtifactPoisoningSink extends DataFlow::Node { // excluding artifacts downloaded to /tmp not download.getPath().regexpMatch("^/tmp.*") and ( - poisonable.(Run).getScriptScalar() = this.asExpr() and + poisonable.(Run).getScript() = this.asExpr() and ( // Check if the poisonable step is a local script execution step // and the path of the command or script matches the path of the downloaded artifact @@ -280,7 +298,7 @@ private module ArtifactPoisoningConfig implements DataFlow::ConfigSig { pred instanceof ArtifactSource and pred.asExpr().(Step).getAFollowingStep() = step and ( - succ.asExpr() = step.(Run).getScriptScalar() or + succ.asExpr() = step.(Run).getScript() or succ.asExpr() = step.(UsesStep) ) ) @@ -288,8 +306,8 @@ private module ArtifactPoisoningConfig implements DataFlow::ConfigSig { exists(Run run | pred instanceof ArtifactSource and pred.asExpr().(Step).getAFollowingStep() = run and - succ.asExpr() = run.getScriptScalar() and - Bash::outputsPartialFileContent(run, run.getACommand()) + succ.asExpr() = run.getScript() and + exists(run.getScript().getAFileReadCommand()) ) } } diff --git a/ql/lib/codeql/actions/security/CodeInjectionQuery.qll b/ql/lib/codeql/actions/security/CodeInjectionQuery.qll index ca72fe00d16..fac498f72da 100644 --- a/ql/lib/codeql/actions/security/CodeInjectionQuery.qll +++ b/ql/lib/codeql/actions/security/CodeInjectionQuery.qll @@ -31,8 +31,8 @@ private module CodeInjectionConfig implements DataFlow::ConfigSig { exists(Run run | pred instanceof FileSource and pred.asExpr().(Step).getAFollowingStep() = run and - succ.asExpr() = run.getScriptScalar() and - Bash::outputsPartialFileContent(run, run.getACommand()) + succ.asExpr() = run.getScript() and + exists(run.getScript().getAFileReadCommand()) ) } } diff --git a/ql/lib/codeql/actions/security/ControlChecks.qll b/ql/lib/codeql/actions/security/ControlChecks.qll index 86de44c3b5c..86c7d989522 100644 --- a/ql/lib/codeql/actions/security/ControlChecks.qll +++ b/ql/lib/codeql/actions/security/ControlChecks.qll @@ -283,8 +283,8 @@ class BashCommentVsHeadDateCheck extends CommentVsHeadDateCheck, Run { BashCommentVsHeadDateCheck() { // eg: if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then exists(string cmd1, string cmd2 | - cmd1 = this.getACommand() and - cmd2 = this.getACommand() and + cmd1 = this.getScript().getACommand() and + cmd2 = this.getScript().getACommand() and not cmd1 = cmd2 and cmd1.toLowerCase().regexpMatch("date\\s+-d.*(commit|pushed|comment|commented)_at.*") and cmd2.toLowerCase().regexpMatch("date\\s+-d.*(commit|pushed|comment|commented)_at.*") diff --git a/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll b/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll index 1f53c938436..859f625e068 100644 --- a/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll +++ b/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll @@ -25,15 +25,15 @@ class EnvPathInjectionFromFileReadSink extends EnvPathInjectionSink { step instanceof UntrustedArtifactDownloadStep or step instanceof PRHeadCheckoutStep ) and - this.asExpr() = run.getScriptScalar() and + this.asExpr() = run.getScript() and step.getAFollowingStep() = run and ( exists(string cmd | - Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_PATH", _) and - Bash::outputsPartialFileContent(run, cmd) + run.getScript().getACmdReachingGitHubPathWrite(cmd) and + run.getScript().getAFileReadCommand() = cmd ) or - Bash::fileToGitHubPath(run, _) + run.getScript().fileToGitHubPath(_) ) ) } @@ -49,9 +49,8 @@ class EnvPathInjectionFromFileReadSink extends EnvPathInjectionSink { class EnvPathInjectionFromCommandSink extends EnvPathInjectionSink { EnvPathInjectionFromCommandSink() { exists(CommandSource source | - this.asExpr() = source.getEnclosingRun().getScriptScalar() and - Bash::cmdReachingGitHubFileWrite(source.getEnclosingRun(), source.getCommand(), "GITHUB_PATH", - _) + this.asExpr() = source.getEnclosingRun().getScript() and + source.getEnclosingRun().getScript().getACmdReachingGitHubPathWrite(source.getCommand()) ) } } @@ -67,9 +66,9 @@ class EnvPathInjectionFromCommandSink extends EnvPathInjectionSink { class EnvPathInjectionFromEnvVarSink extends EnvPathInjectionSink { EnvPathInjectionFromEnvVarSink() { exists(Run run, string var_name | - Bash::envReachingGitHubFileWrite(run, var_name, "GITHUB_PATH", _) and + run.getScript().getAnEnvReachingGitHubPathWrite(var_name) and exists(run.getInScopeEnvVarExpr(var_name)) and - run.getScriptScalar() = this.asExpr() + run.getScript() = this.asExpr() ) } } @@ -90,8 +89,12 @@ private module EnvPathInjectionConfig implements DataFlow::ConfigSig { predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) { exists(Run run, string var | run.getInScopeEnvVarExpr(var) = pred.asExpr() and - succ.asExpr() = run.getScriptScalar() and - Bash::envReachingGitHubFileWrite(run, var, ["GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_PATH"], _) + succ.asExpr() = run.getScript() and + ( + run.getScript().getAnEnvReachingGitHubOutputWrite(var, _) or + run.getScript().getAnEnvReachingGitHubEnvWrite(var, _) or + run.getScript().getAnEnvReachingGitHubPathWrite(var) + ) ) or exists(Uses step | @@ -104,8 +107,8 @@ private module EnvPathInjectionConfig implements DataFlow::ConfigSig { exists(Run run | pred instanceof FileSource and pred.asExpr().(Step).getAFollowingStep() = run and - succ.asExpr() = run.getScriptScalar() and - Bash::outputsPartialFileContent(run, run.getACommand()) + succ.asExpr() = run.getScript() and + exists(run.getScript().getAFileReadCommand()) ) } } diff --git a/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll b/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll index dd6b8342185..214e97fed6b 100644 --- a/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll +++ b/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll @@ -28,15 +28,15 @@ class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink { step instanceof UntrustedArtifactDownloadStep or step instanceof PRHeadCheckoutStep ) and - this.asExpr() = run.getScriptScalar() and + this.asExpr() = run.getScript() and step.getAFollowingStep() = run and ( exists(string cmd | - Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_ENV", _) and - Bash::outputsPartialFileContent(run, cmd) + run.getScript().getACmdReachingGitHubEnvWrite(cmd, _) and + run.getScript().getAFileReadCommand() = cmd ) or - Bash::fileToGitHubEnv(run, _) + run.getScript().fileToGitHubEnv(_) ) ) } @@ -52,9 +52,8 @@ class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink { class EnvVarInjectionFromCommandSink extends EnvVarInjectionSink { EnvVarInjectionFromCommandSink() { exists(CommandSource source | - this.asExpr() = source.getEnclosingRun().getScriptScalar() and - Bash::cmdReachingGitHubFileWrite(source.getEnclosingRun(), source.getCommand(), "GITHUB_ENV", - _) + this.asExpr() = source.getEnclosingRun().getScript() and + source.getEnclosingRun().getScript().getACmdReachingGitHubEnvWrite(source.getCommand(), _) ) } } @@ -71,8 +70,8 @@ class EnvVarInjectionFromEnvVarSink extends EnvVarInjectionSink { EnvVarInjectionFromEnvVarSink() { exists(Run run, string var_name | exists(run.getInScopeEnvVarExpr(var_name)) and - run.getScriptScalar() = this.asExpr() and - Bash::envReachingGitHubFileWrite(run, var_name, "GITHUB_ENV", _) + run.getScript() = this.asExpr() and + run.getScript().getAnEnvReachingGitHubEnvWrite(var_name, _) ) } } @@ -109,8 +108,12 @@ private module EnvVarInjectionConfig implements DataFlow::ConfigSig { predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) { exists(Run run, string var | run.getInScopeEnvVarExpr(var) = pred.asExpr() and - succ.asExpr() = run.getScriptScalar() and - Bash::envReachingGitHubFileWrite(run, var, ["GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_PATH"], _) + succ.asExpr() = run.getScript() and + ( + run.getScript().getAnEnvReachingGitHubEnvWrite(var, _) + or + run.getScript().getAnEnvReachingGitHubOutputWrite(var, _) + ) ) or exists(Uses step | @@ -123,8 +126,8 @@ private module EnvVarInjectionConfig implements DataFlow::ConfigSig { exists(Run run | pred instanceof FileSource and pred.asExpr().(Step).getAFollowingStep() = run and - succ.asExpr() = run.getScriptScalar() and - Bash::outputsPartialFileContent(run, run.getACommand()) + succ.asExpr() = run.getScript() and + exists(run.getScript().getAFileReadCommand()) ) } } diff --git a/ql/lib/codeql/actions/security/OutputClobberingQuery.qll b/ql/lib/codeql/actions/security/OutputClobberingQuery.qll index 4f9eeef7579..e959c7d60ca 100644 --- a/ql/lib/codeql/actions/security/OutputClobberingQuery.qll +++ b/ql/lib/codeql/actions/security/OutputClobberingQuery.qll @@ -19,7 +19,7 @@ abstract class OutputClobberingSink extends DataFlow::Node { } */ class OutputClobberingFromFileReadSink extends OutputClobberingSink { OutputClobberingFromFileReadSink() { - exists(Run run, Step step | + exists(Run run, Step step, string field1, string field2 | ( step instanceof UntrustedArtifactDownloadStep or @@ -31,7 +31,8 @@ class OutputClobberingFromFileReadSink extends OutputClobberingSink { exists(Uses uses | step = uses and uses.getCallee() = "actions/checkout" and - exists(uses.getArgument("ref")) + exists(uses.getArgument("ref")) and + not uses.getArgument("ref").matches("%base%") ) or step instanceof GitMutableRefCheckout @@ -43,14 +44,28 @@ class OutputClobberingFromFileReadSink extends OutputClobberingSink { step instanceof GhSHACheckout ) and step.getAFollowingStep() = run and - this.asExpr() = run.getScriptScalar() and + this.asExpr() = run.getScript() and + // A write to GITHUB_OUTPUT that is not attacker-controlled + exists(string str | + // The output of a command that is not a file read command + run.getScript().getACmdReachingGitHubOutputWrite(str, field1) and + not str = run.getScript().getAFileReadCommand() + or + // A hard-coded string + run.getScript().getAWriteToGitHubOutput(field1, str) and + str.regexpMatch("[\"'0-9a-zA-Z_\\-]+") + ) and + // A write to GITHUB_OUTPUT that is attacker-controlled ( + // echo "sha=$(> $GITHUB_OUTPUT exists(string cmd | - Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_OUTPUT", _) and - Bash::outputsPartialFileContent(run, cmd) + run.getScript().getACmdReachingGitHubOutputWrite(cmd, field2) and + run.getScript().getAFileReadCommand() = cmd ) or - Bash::fileToGitHubOutput(run, _) + // cat test-results/.vars >> $GITHUB_OUTPUT + run.getScript().fileToGitHubOutput(_) and + field2 = "UNKNOWN" ) ) } @@ -66,16 +81,24 @@ class OutputClobberingFromFileReadSink extends OutputClobberingSink { */ class OutputClobberingFromEnvVarSink extends OutputClobberingSink { OutputClobberingFromEnvVarSink() { - exists(Run run, string var, string field | - Bash::envReachingGitHubFileWrite(run, var, "GITHUB_OUTPUT", field) and - // there is a different output variable in the same script - // TODO: key2/value2 should be declared before key/value - exists(string field2 | - run.getAWriteToGitHubOutput(field2, _) and - not field2 = field + exists(Run run, string field1, string field2 | + // A write to GITHUB_OUTPUT that is attacker-controlled + exists(string var | + run.getScript().getAnEnvReachingGitHubOutputWrite(var, field1) and + exists(run.getInScopeEnvVarExpr(var)) and + run.getScript() = this.asExpr() ) and - exists(run.getInScopeEnvVarExpr(var)) and - run.getScriptScalar() = this.asExpr() + // A write to GITHUB_OUTPUT that is not attacker-controlled + exists(string str | + // The output of a command that is not a file read command + run.getScript().getACmdReachingGitHubOutputWrite(str, field2) and + not str = run.getScript().getAFileReadCommand() + or + // A hard-coded string + run.getScript().getAWriteToGitHubOutput(field2, str) and + str.regexpMatch("[\"'0-9a-zA-Z_\\-]+") + ) and + not field2 = field1 ) } } @@ -97,13 +120,18 @@ class OutputClobberingFromEnvVarSink extends OutputClobberingSink { * echo $BODY */ class WorkflowCommandClobberingFromEnvVarSink extends OutputClobberingSink { + string clobbering_var; + string clobbered_value; + WorkflowCommandClobberingFromEnvVarSink() { - exists(Run run, string clobbering_line, string var_name | - Bash::singleLineWorkflowCmd(run.getACommand(), "set-output", _, _) and - run.getACommand() = clobbering_line and - clobbering_line.regexpMatch(".*echo\\s+(-e\\s+)?(\"|')?\\$(\\{)?" + var_name + ".*") and - exists(run.getInScopeEnvVarExpr(var_name)) and - run.getScriptScalar() = this.asExpr() + exists(Run run, string workflow_cmd_stmt, string clobbering_stmt | + run.getScript() = this.asExpr() and + run.getScript().getAStmt() = clobbering_stmt and + clobbering_stmt.regexpMatch("echo\\s+(-e\\s+)?(\"|')?\\$(\\{)?" + clobbering_var + ".*") and + exists(run.getInScopeEnvVarExpr(clobbering_var)) and + run.getScript().getAStmt() = workflow_cmd_stmt and + clobbered_value = + trimQuotes(workflow_cmd_stmt.regexpCapture(".*::set-output\\s+name=.*::(.*)", 1)) ) } } @@ -133,30 +161,35 @@ class WorkflowCommandClobberingFromEnvVarSink extends OutputClobberingSink { * echo "::set-output name=OUTPUT::SAFE" */ class WorkflowCommandClobberingFromFileReadSink extends OutputClobberingSink { + string clobbering_cmd; + WorkflowCommandClobberingFromFileReadSink() { - exists(Run run, string clobbering_line | - run.getScriptScalar() = this.asExpr() and - Bash::singleLineWorkflowCmd(run.getACommand(), "set-output", _, _) and - run.getACommand() = clobbering_line and + exists(Run run, string clobbering_stmt | + run.getScript() = this.asExpr() and + run.getScript().getAStmt() = clobbering_stmt and ( - // A file is read and its content is assigned to an env var that gets printed to stdout + // A file's content is assigned to an env var that gets printed to stdout // - run: | // foo=$(> $GITHUB_OUTPUT echo "OUTPUT_2=$(> $GITHUB_OUTPUT diff --git a/ql/test/query-tests/Security/CWE-077/.github/actions/download-artifact-2/action.yaml b/ql/test/query-tests/Security/CWE-077/.github/actions/download-artifact-2/action.yaml new file mode 100644 index 00000000000..4241647d3e1 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/actions/download-artifact-2/action.yaml @@ -0,0 +1,32 @@ +name: DownloadArtifacts +description: 'Downloads and unarchives artifacts for a workflow that runs on workflow_run so that it can use its data' +runs: + using: "composite" + steps: + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + 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 == "artifacts" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`/tmp/artifacts.zip`, Buffer.from(download.data)); + - run: | + mkdir -p /tmp/artifacts + unzip /tmp/artifacts.zip + shell: bash + - run: | + echo "Downloaded artifacts:" + ls -ablh + shell: bash diff --git a/ql/test/query-tests/Security/CWE-077/.github/actions/download-artifact/action.yaml b/ql/test/query-tests/Security/CWE-077/.github/actions/download-artifact/action.yaml new file mode 100644 index 00000000000..0c205952102 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/actions/download-artifact/action.yaml @@ -0,0 +1,32 @@ +name: DownloadArtifacts +description: 'Downloads and unarchives artifacts for a workflow that runs on workflow_run so that it can use its data' +runs: + using: "composite" + steps: + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + 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 == "artifacts" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`/tmp/artifacts.zip`, Buffer.from(download.data)); + - run: | + mkdir -p /tmp/artifacts + unzip /tmp/artifacts.zip -d /tmp/artifacts + shell: bash + - run: | + echo "Downloaded artifacts:" + ls -ablh /tmp/artifacts + shell: bash diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning51.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning51.yml new file mode 100644 index 00000000000..71f590fbc9c --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning51.yml @@ -0,0 +1,20 @@ +name: Pull Request Open + +on: + workflow_run: + workflows: ["Prev"] + types: + - completed + +jobs: + Download: + runs-on: ubuntu-latest + steps: + - run: | + gh run download "${{github.event.workflow_run.id}}" --repo "${GITHUB_REPOSITORY}" --name "artifact_name" + - name: Unzip + run: | + unzip artifact_name.zip -d foo + - name: Env Var Injection + run: | + echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning52.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning52.yml new file mode 100644 index 00000000000..e4845a6f2f1 --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning52.yml @@ -0,0 +1,26 @@ +name: Pull Request Open + +on: + workflow_run: + workflows: ["Prev"] + types: + - completed + +jobs: + Download: + runs-on: ubuntu-latest + steps: + - run: | + gh run download "${{github.event.workflow_run.id}}" --repo "${GITHUB_REPOSITORY}" --name "artifact_name" + - name: Unzip + run: | + unzip artifact_name.zip -d foo + - name: Env Var Injection + run: | + echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}" + cat foo >> "$GITHUB_ENV" + echo "EOF" >> "${GITHUB_ENV}" + + + + diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning53.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning53.yml new file mode 100644 index 00000000000..67209267b5c --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning53.yml @@ -0,0 +1,27 @@ +name: Pull Request Open + +on: + workflow_run: + workflows: ["Prev"] + types: + - completed + +jobs: + Download: + runs-on: ubuntu-latest + steps: + - run: | + gh run download "${{github.event.workflow_run.id}}" --repo "${GITHUB_REPOSITORY}" --name "artifact_name" + - name: Unzip + run: | + unzip artifact_name.zip -d foo + - run: | + { + echo 'JSON_RESPONSE<> "$GITHUB_ENV" + + + + diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning91.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning91.yml new file mode 100644 index 00000000000..af9f01b572f --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning91.yml @@ -0,0 +1,29 @@ +name: SnapshotPR +on: + workflow_run: + workflows: + - ApprovalComment + types: + - completed +jobs: + snapshot: + permissions: + id-token: write + pull-requests: write + statuses: write + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - uses: ./.github/actions/download-artifact + - id: metadata + run: | + pr_number="$(head -n 2 /tmp/artifacts/metadata.txt | tail -n 1)" + pr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)" + echo PR_COMMIT="$pr_commit" >> "$GITHUB_ENV" + echo PR_NUMBER="$pr_number" >> "$GITHUB_ENV" + - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + ref: ${{ env.PR_COMMIT }} + - uses: ./.github/actions/install-deps + - run: make snapshot diff --git a/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning92.yml b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning92.yml new file mode 100644 index 00000000000..e35bc73c3bd --- /dev/null +++ b/ql/test/query-tests/Security/CWE-077/.github/workflows/artifactpoisoning92.yml @@ -0,0 +1,29 @@ +name: SnapshotPR +on: + workflow_run: + workflows: + - ApprovalComment + types: + - completed +jobs: + snapshot: + permissions: + id-token: write + pull-requests: write + statuses: write + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - uses: ./.github/actions/download-artifact-2 + - id: metadata + run: | + pr_number="$(head -n 2 /tmp/artifacts/metadata.txt | tail -n 1)" + pr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)" + echo PR_COMMIT="$pr_commit" >> "$GITHUB_ENV" + echo PR_NUMBER="$pr_number" >> "$GITHUB_ENV" + - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + ref: ${{ env.PR_COMMIT }} + - uses: ./.github/actions/install-deps + - run: make snapshot diff --git a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected index aff785242f9..220eaf33663 100644 --- a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected +++ b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionCritical.expected @@ -1,4 +1,9 @@ edges +| .github/actions/download-artifact-2/action.yaml:6:7:25:4 | Uses Step | .github/workflows/artifactpoisoning92.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | provenance | Config | +| .github/actions/download-artifact/action.yaml:6:7:25:4 | Uses Step | .github/workflows/artifactpoisoning91.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | provenance | Config | +| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | provenance | Config | +| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | provenance | Config | +| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | provenance | Config | | .github/workflows/test2.yml:12:9:41:6 | Uses Step | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | provenance | Config | | .github/workflows/test3.yml:13:7:20:4 | Uses Step | .github/workflows/test3.yml:20:12:23:77 | echo "PR_NUMBER=$(cat pr_number.txt \| jq -r .)" >> $GITHUB_ENV\necho "PR_HEAD_REPO=$(cat pr_head_repo.txt \| jq -Rr .)" >> $GITHUB_ENV\necho "PR_HEAD_REF=$(cat pr_head_ref.txt \| jq -Rr .)" >> $GITHUB_ENV\n | provenance | Config | | .github/workflows/test4.yml:11:19:11:56 | github.event.pull_request.title | .github/workflows/test4.yml:12:14:13:48 | echo "PR_TITLE=$TITLE" >> $GITHUB_ENV\n | provenance | Config | @@ -25,6 +30,16 @@ edges | .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | Config | | .github/workflows/test12.yml:55:9:61:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | Config | nodes +| .github/actions/download-artifact-2/action.yaml:6:7:25:4 | Uses Step | semmle.label | Uses Step | +| .github/actions/download-artifact/action.yaml:6:7:25:4 | Uses Step | semmle.label | Uses Step | +| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | semmle.label | Run Step | +| .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | semmle.label | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | +| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | semmle.label | Run Step | +| .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | semmle.label | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | +| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | semmle.label | Run Step | +| .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | +| .github/workflows/artifactpoisoning91.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | semmle.label | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | +| .github/workflows/artifactpoisoning92.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | semmle.label | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | | .github/workflows/test2.yml:12:9:41:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | semmle.label | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | | .github/workflows/test3.yml:13:7:20:4 | Uses Step | semmle.label | Uses Step | @@ -79,6 +94,11 @@ nodes | .github/workflows/test15.yml:18:14:20:48 | PR_BODY=$(jq --raw-output .pull_request.body ${GITHUB_EVENT_PATH})\necho "BODY=$PR_BODY" >> "$GITHUB_ENV"\n | semmle.label | PR_BODY=$(jq --raw-output .pull_request.body ${GITHUB_EVENT_PATH})\necho "BODY=$PR_BODY" >> "$GITHUB_ENV"\n | subpaths #select +| .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | +| .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | +| .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | +| .github/workflows/artifactpoisoning91.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | .github/actions/download-artifact/action.yaml:6:7:25:4 | Uses Step | .github/workflows/artifactpoisoning91.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning91.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | +| .github/workflows/artifactpoisoning92.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | .github/actions/download-artifact-2/action.yaml:6:7:25:4 | Uses Step | .github/workflows/artifactpoisoning92.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning92.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | .github/workflows/test2.yml:12:9:41:6 | Uses Step | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | | .github/workflows/test3.yml:20:12:23:77 | echo "PR_NUMBER=$(cat pr_number.txt \| jq -r .)" >> $GITHUB_ENV\necho "PR_HEAD_REPO=$(cat pr_head_repo.txt \| jq -Rr .)" >> $GITHUB_ENV\necho "PR_HEAD_REF=$(cat pr_head_ref.txt \| jq -Rr .)" >> $GITHUB_ENV\n | .github/workflows/test3.yml:13:7:20:4 | Uses Step | .github/workflows/test3.yml:20:12:23:77 | echo "PR_NUMBER=$(cat pr_number.txt \| jq -r .)" >> $GITHUB_ENV\necho "PR_HEAD_REPO=$(cat pr_head_repo.txt \| jq -Rr .)" >> $GITHUB_ENV\necho "PR_HEAD_REF=$(cat pr_head_ref.txt \| jq -Rr .)" >> $GITHUB_ENV\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test3.yml:20:12:23:77 | echo "PR_NUMBER=$(cat pr_number.txt \| jq -r .)" >> $GITHUB_ENV\necho "PR_HEAD_REPO=$(cat pr_head_repo.txt \| jq -Rr .)" >> $GITHUB_ENV\necho "PR_HEAD_REF=$(cat pr_head_ref.txt \| jq -Rr .)" >> $GITHUB_ENV\n | echo "PR_NUMBER=$(cat pr_number.txt \| jq -r .)" >> $GITHUB_ENV\necho "PR_HEAD_REPO=$(cat pr_head_repo.txt \| jq -Rr .)" >> $GITHUB_ENV\necho "PR_HEAD_REF=$(cat pr_head_ref.txt \| jq -Rr .)" >> $GITHUB_ENV\n | | .github/workflows/test4.yml:12:14:13:48 | echo "PR_TITLE=$TITLE" >> $GITHUB_ENV\n | .github/workflows/test4.yml:11:19:11:56 | github.event.pull_request.title | .github/workflows/test4.yml:12:14:13:48 | echo "PR_TITLE=$TITLE" >> $GITHUB_ENV\n | Potential environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test4.yml:12:14:13:48 | echo "PR_TITLE=$TITLE" >> $GITHUB_ENV\n | echo "PR_TITLE=$TITLE" >> $GITHUB_ENV\n | diff --git a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected index 1ac092dd0d3..23bc7784f76 100644 --- a/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected +++ b/ql/test/query-tests/Security/CWE-077/EnvVarInjectionMedium.expected @@ -1,4 +1,9 @@ edges +| .github/actions/download-artifact-2/action.yaml:6:7:25:4 | Uses Step | .github/workflows/artifactpoisoning92.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | provenance | Config | +| .github/actions/download-artifact/action.yaml:6:7:25:4 | Uses Step | .github/workflows/artifactpoisoning91.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | provenance | Config | +| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | provenance | Config | +| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | provenance | Config | +| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | provenance | Config | | .github/workflows/test2.yml:12:9:41:6 | Uses Step | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | provenance | Config | | .github/workflows/test3.yml:13:7:20:4 | Uses Step | .github/workflows/test3.yml:20:12:23:77 | echo "PR_NUMBER=$(cat pr_number.txt \| jq -r .)" >> $GITHUB_ENV\necho "PR_HEAD_REPO=$(cat pr_head_repo.txt \| jq -Rr .)" >> $GITHUB_ENV\necho "PR_HEAD_REF=$(cat pr_head_ref.txt \| jq -Rr .)" >> $GITHUB_ENV\n | provenance | Config | | .github/workflows/test4.yml:11:19:11:56 | github.event.pull_request.title | .github/workflows/test4.yml:12:14:13:48 | echo "PR_TITLE=$TITLE" >> $GITHUB_ENV\n | provenance | Config | @@ -25,6 +30,16 @@ edges | .github/workflows/test12.yml:38:9:46:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | Config | | .github/workflows/test12.yml:55:9:61:6 | Uses Step | .github/workflows/test12.yml:63:14:68:29 | {\n echo 'PRERELEASE_REPORT<> "$GITHUB_ENV"\n | provenance | Config | nodes +| .github/actions/download-artifact-2/action.yaml:6:7:25:4 | Uses Step | semmle.label | Uses Step | +| .github/actions/download-artifact/action.yaml:6:7:25:4 | Uses Step | semmle.label | Uses Step | +| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | semmle.label | Run Step | +| .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | semmle.label | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | +| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | semmle.label | Run Step | +| .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | semmle.label | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | +| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | semmle.label | Run Step | +| .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | +| .github/workflows/artifactpoisoning91.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | semmle.label | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | +| .github/workflows/artifactpoisoning92.yml:20:14:24:55 | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | semmle.label | pr_number="$(head -n 2 /tmp/artifacts/metadata.txt \| tail -n 1)"\npr_commit="$(tail -n 1 /tmp/artifacts/metadata.txt)"\necho PR_COMMIT="$pr_commit" >> "$GITHUB_ENV"\necho PR_NUMBER="$pr_number" >> "$GITHUB_ENV"\n | | .github/workflows/test2.yml:12:9:41:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/test2.yml:41:14:43:52 | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | semmle.label | unzip pr.zip\necho "pr_number=$(cat NR)" >> $GITHUB_ENV\n | | .github/workflows/test3.yml:13:7:20:4 | Uses Step | semmle.label | Uses Step | diff --git a/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningCritical.expected b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningCritical.expected index 7aa170a2e98..7a59ab6ec60 100644 --- a/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningCritical.expected +++ b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningCritical.expected @@ -11,9 +11,6 @@ edges | .github/workflows/artifactpoisoning34.yml:13:9:16:6 | Run Step | .github/workflows/artifactpoisoning34.yml:20:14:22:23 | npm install\nnpm run lint\n | provenance | Config | | .github/workflows/artifactpoisoning41.yml:13:9:21:6 | Run Step | .github/workflows/artifactpoisoning41.yml:22:14:22:22 | ./foo/cmd | provenance | Config | | .github/workflows/artifactpoisoning42.yml:13:9:21:6 | Run Step | .github/workflows/artifactpoisoning42.yml:22:14:22:18 | ./cmd | provenance | Config | -| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | provenance | Config | -| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | provenance | Config | -| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | provenance | Config | | .github/workflows/artifactpoisoning71.yml:9:9:16:6 | Uses Step | .github/workflows/artifactpoisoning71.yml:17:14:18:40 | sed -f config foo.md > bar.md\n | provenance | Config | | .github/workflows/artifactpoisoning81.yml:28:9:31:6 | Uses Step | .github/workflows/artifactpoisoning81.yml:31:14:31:27 | python test.py | provenance | Config | nodes @@ -38,12 +35,6 @@ nodes | .github/workflows/artifactpoisoning41.yml:22:14:22:22 | ./foo/cmd | semmle.label | ./foo/cmd | | .github/workflows/artifactpoisoning42.yml:13:9:21:6 | Run Step | semmle.label | Run Step | | .github/workflows/artifactpoisoning42.yml:22:14:22:18 | ./cmd | semmle.label | ./cmd | -| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | semmle.label | Run Step | -| .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | semmle.label | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | -| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | semmle.label | Run Step | -| .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | semmle.label | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | -| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | semmle.label | Run Step | -| .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | | .github/workflows/artifactpoisoning71.yml:9:9:16:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/artifactpoisoning71.yml:17:14:18:40 | sed -f config foo.md > bar.md\n | semmle.label | sed -f config foo.md > bar.md\n | | .github/workflows/artifactpoisoning81.yml:28:9:31:6 | Uses Step | semmle.label | Uses Step | @@ -62,9 +53,6 @@ subpaths | .github/workflows/artifactpoisoning34.yml:20:14:22:23 | npm install\nnpm run lint\n | .github/workflows/artifactpoisoning34.yml:13:9:16:6 | Run Step | .github/workflows/artifactpoisoning34.yml:20:14:22:23 | npm install\nnpm run lint\n | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning34.yml:20:14:22:23 | npm install\nnpm run lint\n | npm install\nnpm run lint\n | | .github/workflows/artifactpoisoning41.yml:22:14:22:22 | ./foo/cmd | .github/workflows/artifactpoisoning41.yml:13:9:21:6 | Run Step | .github/workflows/artifactpoisoning41.yml:22:14:22:22 | ./foo/cmd | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning41.yml:22:14:22:22 | ./foo/cmd | ./foo/cmd | | .github/workflows/artifactpoisoning42.yml:22:14:22:18 | ./cmd | .github/workflows/artifactpoisoning42.yml:13:9:21:6 | Run Step | .github/workflows/artifactpoisoning42.yml:22:14:22:18 | ./cmd | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning42.yml:22:14:22:18 | ./cmd | ./cmd | -| .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | -| .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | -| .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | | .github/workflows/artifactpoisoning71.yml:17:14:18:40 | sed -f config foo.md > bar.md\n | .github/workflows/artifactpoisoning71.yml:9:9:16:6 | Uses Step | .github/workflows/artifactpoisoning71.yml:17:14:18:40 | sed -f config foo.md > bar.md\n | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning71.yml:17:14:18:40 | sed -f config foo.md > bar.md\n | sed -f config foo.md > bar.md\n | | .github/workflows/artifactpoisoning81.yml:31:14:31:27 | python test.py | .github/workflows/artifactpoisoning81.yml:28:9:31:6 | Uses Step | .github/workflows/artifactpoisoning81.yml:31:14:31:27 | python test.py | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning81.yml:31:14:31:27 | python test.py | python test.py | | .github/workflows/artifactpoisoning92.yml:28:9:29:6 | Uses Step | .github/actions/download-artifact-2/action.yaml:6:7:25:4 | Uses Step | .github/workflows/artifactpoisoning92.yml:28:9:29:6 | Uses Step | Potential artifact poisoning in $@, which may be controlled by an external user. | .github/workflows/artifactpoisoning92.yml:28:9:29:6 | Uses Step | Uses Step | diff --git a/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningMedium.expected b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningMedium.expected index 8d946507799..2ed89bcb4bc 100644 --- a/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningMedium.expected +++ b/ql/test/query-tests/Security/CWE-829/ArtifactPoisoningMedium.expected @@ -11,9 +11,6 @@ edges | .github/workflows/artifactpoisoning34.yml:13:9:16:6 | Run Step | .github/workflows/artifactpoisoning34.yml:20:14:22:23 | npm install\nnpm run lint\n | provenance | Config | | .github/workflows/artifactpoisoning41.yml:13:9:21:6 | Run Step | .github/workflows/artifactpoisoning41.yml:22:14:22:22 | ./foo/cmd | provenance | Config | | .github/workflows/artifactpoisoning42.yml:13:9:21:6 | Run Step | .github/workflows/artifactpoisoning42.yml:22:14:22:18 | ./cmd | provenance | Config | -| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | provenance | Config | -| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | provenance | Config | -| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | provenance | Config | | .github/workflows/artifactpoisoning71.yml:9:9:16:6 | Uses Step | .github/workflows/artifactpoisoning71.yml:17:14:18:40 | sed -f config foo.md > bar.md\n | provenance | Config | | .github/workflows/artifactpoisoning81.yml:28:9:31:6 | Uses Step | .github/workflows/artifactpoisoning81.yml:31:14:31:27 | python test.py | provenance | Config | nodes @@ -38,12 +35,6 @@ nodes | .github/workflows/artifactpoisoning41.yml:22:14:22:22 | ./foo/cmd | semmle.label | ./foo/cmd | | .github/workflows/artifactpoisoning42.yml:13:9:21:6 | Run Step | semmle.label | Run Step | | .github/workflows/artifactpoisoning42.yml:22:14:22:18 | ./cmd | semmle.label | ./cmd | -| .github/workflows/artifactpoisoning51.yml:13:9:15:6 | Run Step | semmle.label | Run Step | -| .github/workflows/artifactpoisoning51.yml:19:14:20:57 | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | semmle.label | echo "pr_number=$(cat foo/bar)" >> $GITHUB_ENV\n | -| .github/workflows/artifactpoisoning52.yml:13:9:15:6 | Run Step | semmle.label | Run Step | -| .github/workflows/artifactpoisoning52.yml:19:14:22:40 | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | semmle.label | echo "PACKAGES_FILE_LIST<> "${GITHUB_ENV}"\ncat foo >> "$GITHUB_ENV"\necho "EOF" >> "${GITHUB_ENV}"\n | -| .github/workflows/artifactpoisoning53.yml:13:9:15:6 | Run Step | semmle.label | Run Step | -| .github/workflows/artifactpoisoning53.yml:18:14:23:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | semmle.label | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | | .github/workflows/artifactpoisoning71.yml:9:9:16:6 | Uses Step | semmle.label | Uses Step | | .github/workflows/artifactpoisoning71.yml:17:14:18:40 | sed -f config foo.md > bar.md\n | semmle.label | sed -f config foo.md > bar.md\n | | .github/workflows/artifactpoisoning81.yml:28:9:31:6 | Uses Step | semmle.label | Uses Step |