mirror of
https://github.com/github/codeql.git
synced 2025-12-26 21:56:39 +01:00
Refactor of Bash functions
This commit is contained in:
@@ -315,6 +315,22 @@ class Run extends Step instanceof RunImpl {
|
||||
}
|
||||
|
||||
predicate getAWriteToGitHubPath(string value) { super.getAWriteToGitHubPath(value) }
|
||||
|
||||
predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) {
|
||||
super.getAnEnvReachingGitHubOutputWrite(var, output_field)
|
||||
}
|
||||
|
||||
predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) {
|
||||
super.getACmdReachingGitHubOutputWrite(cmd, output_field)
|
||||
}
|
||||
|
||||
predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) {
|
||||
super.getAnEnvReachingGitHubEnvWrite(var, output_field)
|
||||
}
|
||||
|
||||
predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) {
|
||||
super.getACmdReachingGitHubEnvWrite(cmd, output_field)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SimpleReferenceExpression extends AstNode instanceof SimpleReferenceExpressionImpl {
|
||||
|
||||
364
ql/lib/codeql/actions/Bash.qll
Normal file
364
ql/lib/codeql/actions/Bash.qll
Normal file
@@ -0,0 +1,364 @@
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.Locations
|
||||
import codeql.actions.config.Config
|
||||
private import codeql.actions.security.ControlChecks
|
||||
|
||||
module Bash {
|
||||
string stmtSeparator() { result = ";" }
|
||||
|
||||
string commandSeparator() { result = ["&&", "||"] }
|
||||
|
||||
string pipeSeparator() { result = "|" }
|
||||
|
||||
string splitSeparators() {
|
||||
result = stmtSeparator() or result = commandSeparator() or result = pipeSeparator()
|
||||
}
|
||||
|
||||
string redirectionSeparator() { result = [">", ">>", "2>", "2>>", ">&", "2>&", "<", "<<<"] }
|
||||
|
||||
string partialFileContentCommand() { result = ["cat", "jq", "yq", "tail", "head"] }
|
||||
|
||||
/** Checks if expr is a bash command substitution */
|
||||
bindingset[expr]
|
||||
predicate isCmdSubstitution(string expr, string cmd) {
|
||||
exists(string regexp |
|
||||
// $(cmd)
|
||||
regexp = "\\$\\(([^)]+)\\)" and
|
||||
cmd = expr.regexpCapture(regexp, 1)
|
||||
or
|
||||
// `cmd`
|
||||
regexp = "`([^`]+)`" and
|
||||
cmd = expr.regexpCapture(regexp, 1)
|
||||
)
|
||||
}
|
||||
|
||||
/** Checks if expr is a bash command substitution */
|
||||
bindingset[expr]
|
||||
predicate containsCmdSubstitution(string expr, string cmd) {
|
||||
exists(string regexp |
|
||||
// $(cmd)
|
||||
regexp = ".*\\$\\(([^)]+)\\).*" and
|
||||
cmd = expr.regexpCapture(regexp, 1)
|
||||
or
|
||||
// `cmd`
|
||||
regexp = ".*`([^`]+)`.*" and
|
||||
cmd = expr.regexpCapture(regexp, 1)
|
||||
)
|
||||
}
|
||||
|
||||
/** Checks if expr is a bash parameter expansion */
|
||||
bindingset[expr]
|
||||
predicate isParameterExpansion(string expr, string parameter, string operator, string params) {
|
||||
exists(string regexp |
|
||||
// $VAR
|
||||
regexp = "\\$([a-zA-Z_][a-zA-Z0-9_]+)\\b" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR}
|
||||
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${!VAR}
|
||||
regexp = "\\$\\{([!#])([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 2) and
|
||||
operator = expr.regexpCapture(regexp, 1) and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR<OP><PARAMS>}, ...
|
||||
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)([#%/:^,\\-+]{1,2})?(.*?)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = expr.regexpCapture(regexp, 2) and
|
||||
params = expr.regexpCapture(regexp, 3)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[expr]
|
||||
predicate containsParameterExpansion(string expr, string parameter, string operator, string params) {
|
||||
exists(string regexp |
|
||||
// $VAR
|
||||
regexp = ".*\\$([a-zA-Z_][a-zA-Z0-9_]+)\\b.*" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR}
|
||||
regexp = ".*\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}.*" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${!VAR}
|
||||
regexp = ".*\\$\\{([!#])([a-zA-Z_][a-zA-Z0-9_]*)\\}.*" and
|
||||
parameter = expr.regexpCapture(regexp, 2) and
|
||||
operator = expr.regexpCapture(regexp, 1) and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR<OP><PARAMS>}, ...
|
||||
regexp = ".*\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)([#%/:^,\\-+]{1,2})?(.*?)\\}.*" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = expr.regexpCapture(regexp, 2) and
|
||||
params = expr.regexpCapture(regexp, 3)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[raw_content]
|
||||
predicate extractVariableAndValue(string raw_content, string key, string value) {
|
||||
exists(string regexp, string content | content = trimQuotes(raw_content) |
|
||||
regexp = "(?msi).*^([a-zA-Z_][a-zA-Z0-9_]*)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\2\\s*$" and
|
||||
key = trimQuotes(content.regexpCapture(regexp, 1)) and
|
||||
value = trimQuotes(content.regexpCapture(regexp, 3))
|
||||
or
|
||||
exists(string line |
|
||||
line = content.splitAt("\n") and
|
||||
regexp = "(?i)^([a-zA-Z_][a-zA-Z0-9_\\-]*)\\s*=\\s*(.*)$" and
|
||||
key = trimQuotes(line.regexpCapture(regexp, 1)) and
|
||||
value = trimQuotes(line.regexpCapture(regexp, 2))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate singleLineFileWrite(
|
||||
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
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
filters = "" and
|
||||
content = script.regexpCapture(regexp, 2)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
cmd = script.regexpCapture(regexp, 3) and
|
||||
key = "" and
|
||||
value = trimQuotes(script.regexpCapture(regexp, 4))
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate heredocFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp |
|
||||
regexp =
|
||||
"(?msi).*^(cat)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\4\\s*$.*" and
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 4)) and
|
||||
content = script.regexpCapture(regexp, 6) and
|
||||
filters = ""
|
||||
or
|
||||
regexp =
|
||||
"(?msi).*^(cat)\\s*(<<|<)\\s*[-]?['\"]?(\\S+)['\"]?\\s*([^>]*)(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*\n(.*?)\n\\3\\s*$.*" and
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 7)) and
|
||||
filters = script.regexpCapture(regexp, 4) and
|
||||
content = script.regexpCapture(regexp, 8)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate linesFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp, string var_name |
|
||||
regexp =
|
||||
"(?msi).*((echo|printf)\\s+['|\"]?(.*?<<(\\S+))['|\"]?\\s*>>\\s*(\\S+)\\s*[\r\n]+)" +
|
||||
"(((.*?)\\s*>>\\s*\\S+\\s*[\r\n]+)+)" +
|
||||
"((echo|printf)\\s+['|\"]?(EOF)['|\"]?\\s*>>\\s*\\S+\\s*[\r\n]*).*" and
|
||||
var_name = trimQuotes(script.regexpCapture(regexp, 3)).regexpReplaceAll("<<\\s*(\\S+)", "") and
|
||||
content =
|
||||
var_name + "=$(" +
|
||||
trimQuotes(script.regexpCapture(regexp, 6))
|
||||
.regexpReplaceAll(">>.*GITHUB_(ENV|OUTPUT)(})?", "")
|
||||
.trim() + ")" and
|
||||
cmd = "echo" and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
filters = ""
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate blockFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp, string first_line, string var_name |
|
||||
regexp =
|
||||
"(?msi).*^\\s*\\{\\s*[\r\n]" +
|
||||
//
|
||||
"(.*?)" +
|
||||
//
|
||||
"(\\s*\\}\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+))\\s*$.*" and
|
||||
first_line = script.regexpCapture(regexp, 1).splitAt("\n", 0).trim() and
|
||||
var_name = first_line.regexpCapture("echo\\s+('|\\\")?(.*)<<.*", 2) and
|
||||
content = var_name + "=$(" + script.regexpCapture(regexp, 1).splitAt("\n").trim() + ")" and
|
||||
not content.indexOf("EOF") > 0 and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
cmd = "echo" and
|
||||
filters = ""
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate multiLineFileWrite(
|
||||
string script, string cmd, string file, string content, string filters
|
||||
) {
|
||||
heredocFileWrite(script, cmd, file, content, filters)
|
||||
or
|
||||
linesFileWrite(script, cmd, file, content, filters)
|
||||
or
|
||||
blockFileWrite(script, cmd, file, content, filters)
|
||||
}
|
||||
|
||||
bindingset[script, file_var]
|
||||
predicate extractFileWrite(string script, string file_var, string content) {
|
||||
// single line assignment
|
||||
exists(string file_expr, string raw_content |
|
||||
isParameterExpansion(file_expr, file_var, _, _) and
|
||||
singleLineFileWrite(script.splitAt("\n"), _, file_expr, raw_content, _) and
|
||||
content = trimQuotes(raw_content)
|
||||
)
|
||||
or
|
||||
// workflow command assignment
|
||||
exists(string key, string value, string cmd |
|
||||
(
|
||||
file_var = "GITHUB_ENV" and
|
||||
cmd = "set-env" and
|
||||
content = key + "=" + value
|
||||
or
|
||||
file_var = "GITHUB_OUTPUT" and
|
||||
cmd = "set-output" and
|
||||
content = key + "=" + value
|
||||
or
|
||||
file_var = "GITHUB_PATH" and
|
||||
cmd = "add-path" and
|
||||
content = value
|
||||
) and
|
||||
singleLineWorkflowCmd(script.splitAt("\n"), cmd, key, value)
|
||||
)
|
||||
or
|
||||
// multiline assignment
|
||||
exists(string file_expr, string raw_content |
|
||||
multiLineFileWrite(script, _, file_expr, raw_content, _) and
|
||||
isParameterExpansion(file_expr, file_var, _, _) and
|
||||
content = trimQuotes(raw_content)
|
||||
)
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
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
|
||||
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=$(<foo.txt)" >> $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) {
|
||||
exists(string file_write_value |
|
||||
(
|
||||
file_var = "GITHUB_ENV" and
|
||||
run.getAWriteToGitHubEnv(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_OUTPUT" and
|
||||
run.getAWriteToGitHubOutput(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_PATH" and
|
||||
field = "PATH" and
|
||||
run.getAWriteToGitHubPath(file_write_value)
|
||||
) and
|
||||
envReachingRunExpr(run, var, file_write_value)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if and environment variable 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 envReachingRunExpr(Run run, 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
|
||||
containsParameterExpansion(value2, var, _, _) and
|
||||
containsParameterExpansion(expr, var2, _, _)
|
||||
)
|
||||
or
|
||||
// var reaches the file write directly
|
||||
// echo "FIELD=${VAR:-default}" >> $GITHUB_ENV (field, file_write_value)
|
||||
containsParameterExpansion(expr, var, _, _)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
exists(string file_write_value |
|
||||
(
|
||||
file_var = "GITHUB_ENV" and
|
||||
run.getAWriteToGitHubEnv(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_OUTPUT" and
|
||||
run.getAWriteToGitHubOutput(field, file_write_value)
|
||||
or
|
||||
file_var = "GITHUB_PATH" and
|
||||
field = "PATH" and
|
||||
run.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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
private import codeql.actions.Ast
|
||||
private import codeql.Locations
|
||||
import codeql.actions.config.Config
|
||||
private import codeql.actions.security.ControlChecks
|
||||
import codeql.actions.config.Config
|
||||
import codeql.actions.Bash
|
||||
|
||||
bindingset[expr]
|
||||
string normalizeExpr(string expr) {
|
||||
@@ -82,239 +83,3 @@ string normalizePath(string path) {
|
||||
*/
|
||||
bindingset[subpath, path]
|
||||
predicate isSubpath(string subpath, string path) { subpath.substring(0, path.length()) = path }
|
||||
|
||||
module Bash {
|
||||
string stmtSeparator() { result = ";" }
|
||||
|
||||
string commandSeparator() { result = ["&&", "||"] }
|
||||
|
||||
string pipeSeparator() { result = "|" }
|
||||
|
||||
string splitSeparators() {
|
||||
result = stmtSeparator() or result = commandSeparator() or result = pipeSeparator()
|
||||
}
|
||||
|
||||
string redirectionSeparator() { result = [">", ">>", "2>", "2>>", ">&", "2>&", "<", "<<<"] }
|
||||
|
||||
string partialFileContentCommand() { result = ["cat", "jq", "yq", "tail", "head"] }
|
||||
|
||||
/** Checks if expr is a bash parameter expansion */
|
||||
bindingset[expr]
|
||||
predicate isBashParameterExpansion(string expr, string parameter, string operator, string params) {
|
||||
exists(string regexp |
|
||||
// $VAR
|
||||
regexp = "\\$([a-zA-Z_][a-zA-Z0-9_]+)\\b" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR}
|
||||
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = "" and
|
||||
params = ""
|
||||
or
|
||||
// ${!VAR}
|
||||
regexp = "\\$\\{([!#])([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 2) and
|
||||
operator = expr.regexpCapture(regexp, 1) and
|
||||
params = ""
|
||||
or
|
||||
// ${VAR<OP><PARAMS>}, ...
|
||||
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)([#%/:^,\\-+]{1,2})?(.*?)\\}" and
|
||||
parameter = expr.regexpCapture(regexp, 1) and
|
||||
operator = expr.regexpCapture(regexp, 2) and
|
||||
params = expr.regexpCapture(regexp, 3)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[raw_content]
|
||||
predicate extractVariableAndValue(string raw_content, string key, string value) {
|
||||
exists(string regexp, string content | content = trimQuotes(raw_content) |
|
||||
regexp = "(?msi).*^([a-zA-Z_][a-zA-Z0-9_]*)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\2\\s*$" and
|
||||
key = trimQuotes(content.regexpCapture(regexp, 1)) and
|
||||
value = trimQuotes(content.regexpCapture(regexp, 3))
|
||||
or
|
||||
exists(string line |
|
||||
line = content.splitAt("\n") and
|
||||
regexp = "(?i)^([a-zA-Z_][a-zA-Z0-9_\\-]*)\\s*=\\s*(.*)$" and
|
||||
key = trimQuotes(line.regexpCapture(regexp, 1)) and
|
||||
value = trimQuotes(line.regexpCapture(regexp, 2))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate singleLineFileWrite(
|
||||
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
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
filters = "" and
|
||||
content = script.regexpCapture(regexp, 2)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
cmd = script.regexpCapture(regexp, 3) and
|
||||
key = "" and
|
||||
value = trimQuotes(script.regexpCapture(regexp, 4))
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate heredocFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp |
|
||||
regexp =
|
||||
"(?msi).*^(cat)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\4\\s*$.*" and
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 4)) and
|
||||
content = script.regexpCapture(regexp, 6) and
|
||||
filters = ""
|
||||
or
|
||||
regexp =
|
||||
"(?msi).*^(cat)\\s*(<<|<)\\s*[-]?['\"]?(\\S+)['\"]?\\s*([^>]*)(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*\n(.*?)\n\\3\\s*$.*" and
|
||||
cmd = script.regexpCapture(regexp, 1) and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 7)) and
|
||||
filters = script.regexpCapture(regexp, 4) and
|
||||
content = script.regexpCapture(regexp, 8)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate linesFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp |
|
||||
regexp =
|
||||
"(?msi).*((echo|printf)\\s+['|\"]?(.*?<<(\\S+))['|\"]?\\s*>>\\s*(\\S+)\\s*[\r\n]+)" +
|
||||
"(((.*?)\\s*>>\\s*\\S+\\s*[\r\n]+)+)" +
|
||||
"((echo|printf)\\s+['|\"]?(EOF)['|\"]?\\s*>>\\s*\\S+\\s*[\r\n]*).*" and
|
||||
content =
|
||||
trimQuotes(script.regexpCapture(regexp, 3)) + "\n" +
|
||||
trimQuotes(script.regexpCapture(regexp, 6)) + "\n" +
|
||||
trimQuotes(script.regexpCapture(regexp, 4)) and
|
||||
cmd = "echo" and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
filters = ""
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate blockFileWrite(string script, string cmd, string file, string content, string filters) {
|
||||
exists(string regexp |
|
||||
regexp =
|
||||
"(?msi).*^\\s*\\{\\s*[\r\n]" +
|
||||
//
|
||||
"(.*?)" +
|
||||
//
|
||||
"(\\s*\\}\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+))\\s*$.*" and
|
||||
content =
|
||||
script
|
||||
.regexpCapture(regexp, 1)
|
||||
.regexpReplaceAll("(?m)^\\s*(echo|printf|write-output)\\s*['\"](.*?)['\"]", "$2")
|
||||
.regexpReplaceAll("(?m)^\\s*(echo|printf|write-output)\\s*", "") and
|
||||
file = trimQuotes(script.regexpCapture(regexp, 5)) and
|
||||
cmd = "echo" and
|
||||
filters = ""
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[script]
|
||||
predicate multiLineFileWrite(
|
||||
string script, string cmd, string file, string content, string filters
|
||||
) {
|
||||
heredocFileWrite(script, cmd, file, content, filters)
|
||||
or
|
||||
linesFileWrite(script, cmd, file, content, filters)
|
||||
or
|
||||
blockFileWrite(script, cmd, file, content, filters)
|
||||
}
|
||||
|
||||
bindingset[script, file_var]
|
||||
predicate extractFileWrite(string script, string file_var, string content) {
|
||||
// single line assignment
|
||||
exists(string file_expr, string raw_content |
|
||||
isBashParameterExpansion(file_expr, file_var, _, _) and
|
||||
singleLineFileWrite(script.splitAt("\n"), _, file_expr, raw_content, _) and
|
||||
content = trimQuotes(raw_content)
|
||||
)
|
||||
or
|
||||
// workflow command assignment
|
||||
exists(string key, string value, string cmd |
|
||||
(
|
||||
file_var = "GITHUB_ENV" and
|
||||
cmd = "set-env" and
|
||||
content = key + "=" + value
|
||||
or
|
||||
file_var = "GITHUB_OUTPUT" and
|
||||
cmd = "set-output" and
|
||||
content = key + "=" + value
|
||||
or
|
||||
file_var = "GITHUB_PATH" and
|
||||
cmd = "add-path" and
|
||||
content = value
|
||||
) and
|
||||
singleLineWorkflowCmd(script.splitAt("\n"), cmd, key, value)
|
||||
)
|
||||
or
|
||||
// multiline assignment
|
||||
exists(string file_expr, string raw_content |
|
||||
multiLineFileWrite(script, _, file_expr, raw_content, _) and
|
||||
isBashParameterExpansion(file_expr, file_var, _, _) and
|
||||
content = trimQuotes(raw_content)
|
||||
)
|
||||
}
|
||||
|
||||
/** Writes the content of the file specified by `path` into a file pointed to by `file_var` */
|
||||
bindingset[script, file_var]
|
||||
predicate fileToFileWrite(string script, string file_var, string path) {
|
||||
exists(string regexp, string line, string file_expr |
|
||||
isBashParameterExpansion(file_expr, file_var, _, _) and
|
||||
regexp =
|
||||
"(?i)(cat)\\s*" + "((?:(?!<<|<<-)[^>\n])+)\\s*" +
|
||||
"(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*" + "(\\S+)" and
|
||||
line = script.splitAt("\n") and
|
||||
path = line.regexpCapture(regexp, 2) and
|
||||
file_expr = trimQuotes(line.regexpCapture(regexp, 5))
|
||||
)
|
||||
}
|
||||
|
||||
predicate fileToGitHubEnv(Run run, string path) {
|
||||
fileToFileWrite(run.getScript(), "GITHUB_ENV", path)
|
||||
}
|
||||
|
||||
predicate fileToGitHubOutput(Run run, string path) {
|
||||
fileToFileWrite(run.getScript(), "GITHUB_OUTPUT", path)
|
||||
}
|
||||
|
||||
predicate fileToGitHubPath(Run run, string path) {
|
||||
fileToFileWrite(run.getScript(), "GITHUB_PATH", path)
|
||||
}
|
||||
|
||||
bindingset[snippet]
|
||||
predicate outputsPartialFileContent(Run run, string snippet) {
|
||||
// e.g.
|
||||
// echo FOO=`yq '.foo' foo.yml` >> $GITHUB_ENV
|
||||
// echo "FOO=$(<foo.txt)" >> $GITHUB_ENV
|
||||
// yq '.foo' foo.yml >> $GITHUB_PATH
|
||||
// cat foo.txt >> $GITHUB_PATH
|
||||
// Bash::getACommand(snippet).indexOf(["<", Bash::partialFileContentCommand() + " "]) = 0
|
||||
exists(int i, string line, string cmd |
|
||||
run.getStmt(i) = line and
|
||||
line.matches("%" + snippet + "%") and
|
||||
run.getCommand(i) = cmd and
|
||||
cmd.indexOf(["<", Bash::partialFileContentCommand() + " "]) = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1526,6 +1526,22 @@ class RunImpl extends StepImpl {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,23 @@ abstract class ArgumentInjectionSink extends DataFlow::Node {
|
||||
abstract string getCommand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if an environment variable is used, directly or indirectly, as an argument to a dangerous command
|
||||
* in a Run step.
|
||||
* Where the command is a string captured from the Run's script.
|
||||
*/
|
||||
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
|
||||
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
|
||||
exists(run.getInScopeEnvVarExpr(var))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it as the argument to a command vulnerable to argument injection.
|
||||
* e.g.
|
||||
@@ -21,10 +38,10 @@ class ArgumentInjectionFromEnvVarSink extends ArgumentInjectionSink {
|
||||
string command;
|
||||
|
||||
ArgumentInjectionFromEnvVarSink() {
|
||||
exists(Run run, string var_name |
|
||||
envToArgInjSink(var_name, run, command) and
|
||||
exists(Run run, string var |
|
||||
envToArgInjSink(var, run, command) and
|
||||
run.getScriptScalar() = this.asExpr() and
|
||||
exists(run.getInScopeEnvVarExpr(var_name))
|
||||
exists(run.getInScopeEnvVarExpr(var))
|
||||
)
|
||||
or
|
||||
exists(
|
||||
@@ -42,6 +59,33 @@ class ArgumentInjectionFromEnvVarSink extends ArgumentInjectionSink {
|
||||
override string getCommand() { result = command }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step executes a command that returns untrusted data which flows to an unsafe argument
|
||||
* e.g.
|
||||
* run: |
|
||||
* BODY=$(git log --format=%s)
|
||||
* sed "s/FOO/$BODY/g" > /tmp/foo
|
||||
*/
|
||||
class ArgumentInjectionFromCommandSink extends ArgumentInjectionSink {
|
||||
string command;
|
||||
|
||||
ArgumentInjectionFromCommandSink() {
|
||||
exists(
|
||||
CommandSource source, Run run, string cmd, string argument, string regexp, int argument_group,
|
||||
int command_group
|
||||
|
|
||||
run = source.getEnclosingRun() and
|
||||
this.asExpr() = run.getScriptScalar() and
|
||||
cmd = run.getACommand() and
|
||||
argumentInjectionSinksDataModel(regexp, command_group, argument_group) and
|
||||
argument = cmd.regexpCapture(regexp, argument_group) and
|
||||
command = cmd.regexpCapture(regexp, command_group)
|
||||
)
|
||||
}
|
||||
|
||||
override string getCommand() { result = command }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it as the argument to a command vulnerable to argument injection.
|
||||
*/
|
||||
@@ -71,6 +115,14 @@ private module ArgumentInjectionConfig implements DataFlow::ConfigSig {
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof ArgumentInjectionSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run, string var |
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
envToArgInjSink(var, run, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate a code script. */
|
||||
|
||||
@@ -274,6 +274,24 @@ private module ArtifactPoisoningConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof ArtifactSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof ArtifactPoisoningSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(PoisonableStep step |
|
||||
pred instanceof ArtifactSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
(
|
||||
succ.asExpr() = step.(Run).getScriptScalar() or
|
||||
succ.asExpr() = step.(UsesStep)
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof ArtifactSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
Bash::outputsPartialFileContent(run, run.getACommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe artifacts that is used in an insecure way. */
|
||||
|
||||
@@ -19,6 +19,22 @@ private module CodeInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "code-injection")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
Bash::outputsPartialFileContent(run, run.getACommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate a code script. */
|
||||
|
||||
@@ -14,6 +14,9 @@ abstract class EnvPathInjectionSink extends DataFlow::Node { }
|
||||
* e.g.
|
||||
* run: |
|
||||
* cat foo.txt >> $GITHUB_PATH
|
||||
* echo "$(cat foo.txt)" >> $GITHUB_PATH
|
||||
* FOO=$(cat foo.txt)
|
||||
* echo "$FOO" >> $GITHUB_PATH
|
||||
*/
|
||||
class EnvPathInjectionFromFileReadSink extends EnvPathInjectionSink {
|
||||
EnvPathInjectionFromFileReadSink() {
|
||||
@@ -25,35 +28,34 @@ class EnvPathInjectionFromFileReadSink extends EnvPathInjectionSink {
|
||||
this.asExpr() = run.getScriptScalar() and
|
||||
step.getAFollowingStep() = run and
|
||||
(
|
||||
// e.g.
|
||||
// cat test-results/.env >> $GITHUB_PATH
|
||||
Bash::fileToGitHubPath(run, _)
|
||||
or
|
||||
exists(string value |
|
||||
run.getAWriteToGitHubPath(value) and
|
||||
(
|
||||
Bash::outputsPartialFileContent(run, value)
|
||||
or
|
||||
// e.g.
|
||||
// FOO=$(cat test-results/sha-number)
|
||||
// echo "FOO=$FOO" >> $GITHUB_PATH
|
||||
exists(string var_name, string var_value |
|
||||
run.getAnAssignment(var_name, var_value) and
|
||||
Bash::outputsPartialFileContent(run, var_value) and
|
||||
(
|
||||
value.matches("%$" + ["", "{", "ENV{"] + var_name + "%")
|
||||
or
|
||||
value.regexpMatch("\\$\\((echo|printf|write-output)\\s+.*") and
|
||||
value.indexOf(var_name) > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
exists(string cmd |
|
||||
Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_PATH", _) and
|
||||
Bash::outputsPartialFileContent(run, cmd)
|
||||
)
|
||||
or
|
||||
Bash::fileToGitHubPath(run, _)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step executes a command that returns untrusted data which flows to GITHUB_ENV
|
||||
* e.g.
|
||||
* run: |
|
||||
* COMMIT_MESSAGE=$(git log --format=%s)
|
||||
* echo "${COMMIT_MESSAGE}" >> $GITHUB_PATH
|
||||
*/
|
||||
class EnvPathInjectionFromCommandSink extends EnvPathInjectionSink {
|
||||
EnvPathInjectionFromCommandSink() {
|
||||
exists(CommandSource source |
|
||||
this.asExpr() = source.getEnclosingRun().getScriptScalar() and
|
||||
Bash::cmdReachingGitHubFileWrite(source.getEnclosingRun(), source.getCommand(), "GITHUB_PATH",
|
||||
_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it to declare a PATH env var.
|
||||
* e.g.
|
||||
@@ -65,7 +67,7 @@ class EnvPathInjectionFromFileReadSink extends EnvPathInjectionSink {
|
||||
class EnvPathInjectionFromEnvVarSink extends EnvPathInjectionSink {
|
||||
EnvPathInjectionFromEnvVarSink() {
|
||||
exists(Run run, string var_name |
|
||||
envToSpecialFile("GITHUB_PATH", var_name, run, _) and
|
||||
Bash::envReachingGitHubFileWrite(run, var_name, "GITHUB_PATH", _) and
|
||||
exists(run.getInScopeEnvVarExpr(var_name)) and
|
||||
run.getScriptScalar() = this.asExpr()
|
||||
)
|
||||
@@ -84,6 +86,28 @@ private module EnvPathInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof EnvPathInjectionSink }
|
||||
|
||||
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"], _)
|
||||
)
|
||||
or
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "envpath-injection")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
Bash::outputsPartialFileContent(run, run.getACommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate the PATH environment variable. */
|
||||
|
||||
@@ -14,8 +14,12 @@ abstract class EnvVarInjectionSink extends DataFlow::Node { }
|
||||
* e.g.
|
||||
* run: |
|
||||
* cat test-results/.env >> $GITHUB_ENV
|
||||
*
|
||||
* echo "sha=$(cat test-results/sha-number)" >> $GITHUB_ENV
|
||||
* echo "sha=$(<test-results/sha-number)" >> $GITHUB_ENV
|
||||
*
|
||||
* FOO=$(cat test-results/sha-number)
|
||||
* echo "FOO=$FOO" >> $GITHUB_ENV
|
||||
*/
|
||||
class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink {
|
||||
EnvVarInjectionFromFileReadSink() {
|
||||
@@ -27,37 +31,34 @@ class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink {
|
||||
this.asExpr() = run.getScriptScalar() and
|
||||
step.getAFollowingStep() = run and
|
||||
(
|
||||
// e.g.
|
||||
// cat test-results/.env >> $GITHUB_ENV
|
||||
Bash::fileToGitHubEnv(run, _)
|
||||
or
|
||||
exists(string value |
|
||||
run.getAWriteToGitHubEnv(_, value) and
|
||||
(
|
||||
// e.g.
|
||||
// echo "FOO=$(cat test-results/sha-number)" >> $GITHUB_ENV
|
||||
Bash::outputsPartialFileContent(run, value)
|
||||
or
|
||||
// e.g.
|
||||
// FOO=$(cat test-results/sha-number)
|
||||
// echo "FOO=$FOO" >> $GITHUB_ENV
|
||||
exists(string var_name, string var_value |
|
||||
run.getAnAssignment(var_name, var_value) and
|
||||
Bash::outputsPartialFileContent(run, var_value) and
|
||||
(
|
||||
value.matches("%$" + ["", "{", "ENV{"] + var_name + "%")
|
||||
or
|
||||
value.regexpMatch("\\$\\((echo|printf|write-output)\\s+.*") and
|
||||
value.indexOf(var_name) > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
exists(string cmd |
|
||||
Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_ENV", _) and
|
||||
Bash::outputsPartialFileContent(run, cmd)
|
||||
)
|
||||
or
|
||||
Bash::fileToGitHubEnv(run, _)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step executes a command that returns untrusted data which flows to GITHUB_ENV
|
||||
* e.g.
|
||||
* run: |
|
||||
* COMMIT_MESSAGE=$(git log --format=%s)
|
||||
* echo "COMMIT_MESSAGE=${COMMIT_MESSAGE}" >> $GITHUB_ENV
|
||||
*/
|
||||
class EnvVarInjectionFromCommandSink extends EnvVarInjectionSink {
|
||||
EnvVarInjectionFromCommandSink() {
|
||||
exists(CommandSource source |
|
||||
this.asExpr() = source.getEnclosingRun().getScriptScalar() and
|
||||
Bash::cmdReachingGitHubFileWrite(source.getEnclosingRun(), source.getCommand(), "GITHUB_ENV",
|
||||
_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it to declare env var.
|
||||
* e.g.
|
||||
@@ -69,9 +70,9 @@ class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink {
|
||||
class EnvVarInjectionFromEnvVarSink extends EnvVarInjectionSink {
|
||||
EnvVarInjectionFromEnvVarSink() {
|
||||
exists(Run run, string var_name |
|
||||
envToSpecialFile("GITHUB_ENV", var_name, run, _) and
|
||||
exists(run.getInScopeEnvVarExpr(var_name)) and
|
||||
run.getScriptScalar() = this.asExpr()
|
||||
run.getScriptScalar() = this.asExpr() and
|
||||
Bash::envReachingGitHubFileWrite(run, var_name, "GITHUB_ENV", _)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -104,6 +105,28 @@ private module EnvVarInjectionConfig implements DataFlow::ConfigSig {
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof EnvVarInjectionSink }
|
||||
|
||||
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"], _)
|
||||
)
|
||||
or
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "envvar-injection")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
Bash::outputsPartialFileContent(run, run.getACommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate an environment variable. */
|
||||
|
||||
@@ -10,7 +10,7 @@ import codeql.actions.dataflow.FlowSources
|
||||
abstract class OutputClobberingSink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable with contents from a local file.
|
||||
* Holds if a Run step declares a step output variable with contents from a local file.
|
||||
* e.g.
|
||||
* run: |
|
||||
* cat test-results/.vars >> $GITHUB_OUTPUT
|
||||
@@ -21,58 +21,43 @@ class OutputClobberingFromFileReadSink extends OutputClobberingSink {
|
||||
OutputClobberingFromFileReadSink() {
|
||||
exists(Run run, Step step |
|
||||
(
|
||||
step instanceof UntrustedArtifactDownloadStep or
|
||||
step instanceof UntrustedArtifactDownloadStep
|
||||
or
|
||||
// This shoould be:
|
||||
// artifact instanceof PRHeadCheckoutStep
|
||||
// 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
|
||||
step.(Uses).getCallee() = "actions/checkout" or
|
||||
step instanceof GitMutableRefCheckout or
|
||||
step instanceof GitSHACheckout or
|
||||
step instanceof GhMutableRefCheckout or
|
||||
exists(Uses uses |
|
||||
step = uses and
|
||||
uses.getCallee() = "actions/checkout" and
|
||||
exists(uses.getArgument("ref"))
|
||||
)
|
||||
or
|
||||
step instanceof GitMutableRefCheckout
|
||||
or
|
||||
step instanceof GitSHACheckout
|
||||
or
|
||||
step instanceof GhMutableRefCheckout
|
||||
or
|
||||
step instanceof GhSHACheckout
|
||||
) and
|
||||
this.asExpr() = run.getScriptScalar() and
|
||||
step.getAFollowingStep() = run and
|
||||
this.asExpr() = run.getScriptScalar() and
|
||||
(
|
||||
// e.g.
|
||||
// cat test-results/.vars >> $GITHUB_OUTPUT
|
||||
Bash::fileToGitHubOutput(run, _)
|
||||
or
|
||||
exists(string key, string value |
|
||||
run.getAWriteToGitHubOutput(key, value) and
|
||||
// there is a different output variable in the same script
|
||||
// TODO: key2/value2 should be declared before key/value
|
||||
exists(string key2 |
|
||||
run.getAWriteToGitHubOutput(key2, _) and
|
||||
not key2 = key
|
||||
) and
|
||||
(
|
||||
Bash::outputsPartialFileContent(run, value)
|
||||
or
|
||||
// e.g.
|
||||
// FOO=$(cat test-results/sha-number)
|
||||
// echo "FOO=$FOO" >> $GITHUB_OUTPUT
|
||||
exists(string var_name, string var_value |
|
||||
run.getAnAssignment(var_name, var_value) and
|
||||
Bash::outputsPartialFileContent(run, var_value) and
|
||||
(
|
||||
value.matches("%$" + ["", "{", "ENV{"] + var_name + "%")
|
||||
or
|
||||
value.regexpMatch("\\$\\((echo|printf|write-output)\\s+.*") and
|
||||
value.indexOf(var_name) > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
exists(string cmd |
|
||||
Bash::cmdReachingGitHubFileWrite(run, cmd, "GITHUB_OUTPUT", _) and
|
||||
Bash::outputsPartialFileContent(run, cmd)
|
||||
)
|
||||
or
|
||||
Bash::fileToGitHubOutput(run, _)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a Run step declares an environment variable, uses it to declare env var.
|
||||
* Holds if a Run step declares an environment variable, uses it in a step variable output.
|
||||
* e.g.
|
||||
* env:
|
||||
* BODY: ${{ github.event.comment.body }}
|
||||
@@ -81,15 +66,15 @@ class OutputClobberingFromFileReadSink extends OutputClobberingSink {
|
||||
*/
|
||||
class OutputClobberingFromEnvVarSink extends OutputClobberingSink {
|
||||
OutputClobberingFromEnvVarSink() {
|
||||
exists(Run run, string var_name, string key |
|
||||
envToSpecialFile("GITHUB_OUTPUT", var_name, run, key) and
|
||||
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 key2 |
|
||||
run.getAWriteToGitHubOutput(key2, _) and
|
||||
not key2 = key
|
||||
exists(string field2 |
|
||||
run.getAWriteToGitHubOutput(field2, _) and
|
||||
not field2 = field
|
||||
) and
|
||||
exists(run.getInScopeEnvVarExpr(var_name)) and
|
||||
exists(run.getInScopeEnvVarExpr(var)) and
|
||||
run.getScriptScalar() = this.asExpr()
|
||||
)
|
||||
}
|
||||
@@ -113,10 +98,9 @@ class OutputClobberingFromEnvVarSink extends OutputClobberingSink {
|
||||
*/
|
||||
class WorkflowCommandClobberingFromEnvVarSink extends OutputClobberingSink {
|
||||
WorkflowCommandClobberingFromEnvVarSink() {
|
||||
exists(Run run, string output_line, string clobbering_line, string var_name |
|
||||
run.getScript().splitAt("\n") = output_line and
|
||||
Bash::singleLineWorkflowCmd(output_line, "set-output", _, _) and
|
||||
run.getScript().splitAt("\n") = clobbering_line and
|
||||
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()
|
||||
@@ -124,13 +108,36 @@ class WorkflowCommandClobberingFromEnvVarSink extends OutputClobberingSink {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* - id: clob1
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* PR="$(<pr-number)"
|
||||
* echo "$PR"
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* - id: clob2
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* cat pr-number
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* - id: clob3
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
* ls *.txt
|
||||
* - id: clob4
|
||||
* run: |
|
||||
* # VULNERABLE
|
||||
* CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
|
||||
* echo "$CURRENT_VERSION"
|
||||
* echo "::set-output name=OUTPUT::SAFE"
|
||||
*/
|
||||
class WorkflowCommandClobberingFromFileReadSink extends OutputClobberingSink {
|
||||
WorkflowCommandClobberingFromFileReadSink() {
|
||||
exists(Run run, string output_line, string clobbering_line |
|
||||
exists(Run run, string clobbering_line |
|
||||
run.getScriptScalar() = this.asExpr() and
|
||||
run.getScript().splitAt("\n") = output_line and
|
||||
Bash::singleLineWorkflowCmd(output_line, "set-output", _, _) and
|
||||
run.getScript().splitAt("\n") = clobbering_line and
|
||||
Bash::singleLineWorkflowCmd(run.getACommand(), "set-output", _, _) and
|
||||
run.getACommand() = clobbering_line and
|
||||
(
|
||||
// A file is read and its content is assigned to an env var that gets printed to stdout
|
||||
// - run: |
|
||||
@@ -170,6 +177,31 @@ private module OutputClobberingConfig implements DataFlow::ConfigSig {
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof OutputClobberingSink }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run, string var |
|
||||
run.getInScopeEnvVarExpr(var) = pred.asExpr() and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
run.getAWriteToGitHubOutput(_, _)
|
||||
)
|
||||
or
|
||||
exists(Uses step |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = step and
|
||||
succ.asExpr() = step and
|
||||
madSink(succ, "output-clobbering")
|
||||
)
|
||||
or
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
(
|
||||
Bash::outputsPartialFileContent(run, run.getACommand()) or
|
||||
Bash::singleLineWorkflowCmd(run.getACommand(), "set-output", _, _)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks flow of unsafe user input that is used to construct and evaluate an environment variable. */
|
||||
|
||||
@@ -23,8 +23,8 @@ class PoisonableCommandStep extends PoisonableStep, Run {
|
||||
}
|
||||
}
|
||||
|
||||
class JavascriptImportnUsesStep extends PoisonableStep, UsesStep {
|
||||
JavascriptImportnUsesStep() {
|
||||
class JavascriptImportUsesStep extends PoisonableStep, UsesStep {
|
||||
JavascriptImportUsesStep() {
|
||||
exists(string script, string line, string import_stmt |
|
||||
this.getCallee() = "actions/github-script" and
|
||||
script = this.getArgument("script") and
|
||||
@@ -35,6 +35,13 @@ class JavascriptImportnUsesStep extends PoisonableStep, UsesStep {
|
||||
}
|
||||
}
|
||||
|
||||
class SetupNodeUsesStep extends PoisonableStep, UsesStep {
|
||||
SetupNodeUsesStep() {
|
||||
this.getCallee() = "actions/setup-node" and
|
||||
this.getArgument("cache") = "yarn"
|
||||
}
|
||||
}
|
||||
|
||||
class LocalScriptExecutionRunStep extends PoisonableStep, Run {
|
||||
string path;
|
||||
|
||||
|
||||
@@ -56,6 +56,15 @@ private module ActionsMutableRefCheckoutConfig implements DataFlow::ConfigSig {
|
||||
uses.getArgumentExpr("ref") = sink.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
Bash::outputsPartialFileContent(run, run.getACommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module ActionsMutableRefCheckoutFlow = TaintTracking::Global<ActionsMutableRefCheckoutConfig>;
|
||||
@@ -93,6 +102,15 @@ private module ActionsSHACheckoutConfig implements DataFlow::ConfigSig {
|
||||
uses.getArgumentExpr("ref") = sink.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Run run |
|
||||
pred instanceof FileSource and
|
||||
pred.asExpr().(Step).getAFollowingStep() = run and
|
||||
succ.asExpr() = run.getScriptScalar() and
|
||||
Bash::outputsPartialFileContent(run, run.getACommand())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module ActionsSHACheckoutFlow = TaintTracking::Global<ActionsSHACheckoutConfig>;
|
||||
@@ -139,7 +157,7 @@ predicate containsHeadSHA(string s) {
|
||||
"\\bgithub\\.event\\.merge_group\\.head_sha\\b",
|
||||
"\\bgithub\\.event\\.merge_group\\.head_commit\\.id\\b",
|
||||
// heuristics
|
||||
"\\bhead\\.sha\\b", "\\bhead_sha\\b", "\\bpr_head_sha\\b"
|
||||
"\\bhead\\.sha\\b", "\\bhead_sha\\b", "\\bmerge_sha\\b", "\\bpr_head_sha\\b"
|
||||
], _, _)
|
||||
)
|
||||
}
|
||||
@@ -156,7 +174,7 @@ predicate containsHeadRef(string s) {
|
||||
"\\bgithub\\.event\\.check_run\\.pull_requests\\[\\d+\\]\\.head\\.ref\\b",
|
||||
"\\bgithub\\.event\\.merge_group\\.head_ref\\b",
|
||||
// heuristics
|
||||
"\\bhead\\.ref\\b", "\\bhead_ref\\b", "\\bpr_head_ref\\b",
|
||||
"\\bhead\\.ref\\b", "\\bhead_ref\\b", "\\bmerge_ref\\b", "\\bpr_head_ref\\b",
|
||||
// env vars
|
||||
"GITHUB_HEAD_REF",
|
||||
], _, _)
|
||||
|
||||
Reference in New Issue
Block a user