Skip to content

Commit ba5e1ed

Browse files
author
Alvaro Muñoz
authored
Merge pull request #102 from github/moar_poisonable_steps
Major refactor
2 parents 898507e + 99e92af commit ba5e1ed

File tree

59 files changed

+1556
-918
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1556
-918
lines changed

ql/lib/codeql/actions/Ast.qll

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,22 @@ class Run extends Step instanceof RunImpl {
315315
}
316316

317317
predicate getAWriteToGitHubPath(string value) { super.getAWriteToGitHubPath(value) }
318+
319+
predicate getAnEnvReachingGitHubOutputWrite(string var, string output_field) {
320+
super.getAnEnvReachingGitHubOutputWrite(var, output_field)
321+
}
322+
323+
predicate getACmdReachingGitHubOutputWrite(string cmd, string output_field) {
324+
super.getACmdReachingGitHubOutputWrite(cmd, output_field)
325+
}
326+
327+
predicate getAnEnvReachingGitHubEnvWrite(string var, string output_field) {
328+
super.getAnEnvReachingGitHubEnvWrite(var, output_field)
329+
}
330+
331+
predicate getACmdReachingGitHubEnvWrite(string cmd, string output_field) {
332+
super.getACmdReachingGitHubEnvWrite(cmd, output_field)
333+
}
318334
}
319335

320336
abstract class SimpleReferenceExpression extends AstNode instanceof SimpleReferenceExpressionImpl {

ql/lib/codeql/actions/Bash.qll

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
private import codeql.actions.Ast
2+
private import codeql.Locations
3+
import codeql.actions.config.Config
4+
private import codeql.actions.security.ControlChecks
5+
6+
module Bash {
7+
string stmtSeparator() { result = ";" }
8+
9+
string commandSeparator() { result = ["&&", "||"] }
10+
11+
string pipeSeparator() { result = "|" }
12+
13+
string splitSeparators() {
14+
result = stmtSeparator() or result = commandSeparator() or result = pipeSeparator()
15+
}
16+
17+
string redirectionSeparator() { result = [">", ">>", "2>", "2>>", ">&", "2>&", "<", "<<<"] }
18+
19+
string partialFileContentCommand() { result = ["cat", "jq", "yq", "tail", "head"] }
20+
21+
/** Checks if expr is a bash command substitution */
22+
bindingset[expr]
23+
predicate isCmdSubstitution(string expr, string cmd) {
24+
exists(string regexp |
25+
// $(cmd)
26+
regexp = "\\$\\(([^)]+)\\)" and
27+
cmd = expr.regexpCapture(regexp, 1)
28+
or
29+
// `cmd`
30+
regexp = "`([^`]+)`" and
31+
cmd = expr.regexpCapture(regexp, 1)
32+
)
33+
}
34+
35+
/** Checks if expr is a bash command substitution */
36+
bindingset[expr]
37+
predicate containsCmdSubstitution(string expr, string cmd) {
38+
exists(string regexp |
39+
// $(cmd)
40+
regexp = ".*\\$\\(([^)]+)\\).*" and
41+
cmd = expr.regexpCapture(regexp, 1)
42+
or
43+
// `cmd`
44+
regexp = ".*`([^`]+)`.*" and
45+
cmd = expr.regexpCapture(regexp, 1)
46+
)
47+
}
48+
49+
/** Checks if expr is a bash parameter expansion */
50+
bindingset[expr]
51+
predicate isParameterExpansion(string expr, string parameter, string operator, string params) {
52+
exists(string regexp |
53+
// $VAR
54+
regexp = "\\$([a-zA-Z_][a-zA-Z0-9_]+)\\b" and
55+
parameter = expr.regexpCapture(regexp, 1) and
56+
operator = "" and
57+
params = ""
58+
or
59+
// ${VAR}
60+
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
61+
parameter = expr.regexpCapture(regexp, 1) and
62+
operator = "" and
63+
params = ""
64+
or
65+
// ${!VAR}
66+
regexp = "\\$\\{([!#])([a-zA-Z_][a-zA-Z0-9_]*)\\}" and
67+
parameter = expr.regexpCapture(regexp, 2) and
68+
operator = expr.regexpCapture(regexp, 1) and
69+
params = ""
70+
or
71+
// ${VAR<OP><PARAMS>}, ...
72+
regexp = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)([#%/:^,\\-+]{1,2})?(.*?)\\}" and
73+
parameter = expr.regexpCapture(regexp, 1) and
74+
operator = expr.regexpCapture(regexp, 2) and
75+
params = expr.regexpCapture(regexp, 3)
76+
)
77+
}
78+
79+
bindingset[expr]
80+
predicate containsParameterExpansion(string expr, string parameter, string operator, string params) {
81+
exists(string regexp |
82+
// $VAR
83+
regexp = ".*\\$([a-zA-Z_][a-zA-Z0-9_]+)\\b.*" and
84+
parameter = expr.regexpCapture(regexp, 1) and
85+
operator = "" and
86+
params = ""
87+
or
88+
// ${VAR}
89+
regexp = ".*\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}.*" and
90+
parameter = expr.regexpCapture(regexp, 1) and
91+
operator = "" and
92+
params = ""
93+
or
94+
// ${!VAR}
95+
regexp = ".*\\$\\{([!#])([a-zA-Z_][a-zA-Z0-9_]*)\\}.*" and
96+
parameter = expr.regexpCapture(regexp, 2) and
97+
operator = expr.regexpCapture(regexp, 1) and
98+
params = ""
99+
or
100+
// ${VAR<OP><PARAMS>}, ...
101+
regexp = ".*\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)([#%/:^,\\-+]{1,2})?(.*?)\\}.*" and
102+
parameter = expr.regexpCapture(regexp, 1) and
103+
operator = expr.regexpCapture(regexp, 2) and
104+
params = expr.regexpCapture(regexp, 3)
105+
)
106+
}
107+
108+
bindingset[raw_content]
109+
predicate extractVariableAndValue(string raw_content, string key, string value) {
110+
exists(string regexp, string content | content = trimQuotes(raw_content) |
111+
regexp = "(?msi).*^([a-zA-Z_][a-zA-Z0-9_]*)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\2\\s*$" and
112+
key = trimQuotes(content.regexpCapture(regexp, 1)) and
113+
value = trimQuotes(content.regexpCapture(regexp, 3))
114+
or
115+
exists(string line |
116+
line = content.splitAt("\n") and
117+
regexp = "(?i)^([a-zA-Z_][a-zA-Z0-9_\\-]*)\\s*=\\s*(.*)$" and
118+
key = trimQuotes(line.regexpCapture(regexp, 1)) and
119+
value = trimQuotes(line.regexpCapture(regexp, 2))
120+
)
121+
)
122+
}
123+
124+
bindingset[script]
125+
predicate singleLineFileWrite(
126+
string script, string cmd, string file, string content, string filters
127+
) {
128+
exists(string regexp |
129+
regexp =
130+
"(?i)(echo|printf|write-output)\\s*(.*?)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)" and
131+
cmd = script.regexpCapture(regexp, 1) and
132+
file = trimQuotes(script.regexpCapture(regexp, 5)) and
133+
filters = "" and
134+
content = script.regexpCapture(regexp, 2)
135+
)
136+
}
137+
138+
bindingset[script]
139+
predicate singleLineWorkflowCmd(string script, string cmd, string key, string value) {
140+
exists(string regexp |
141+
regexp =
142+
"(?i)(echo|printf|write-output)\\s*(['|\"])?::(set-[a-z]+)\\s*name\\s*=\\s*(.*?)::(.*)" and
143+
cmd = script.regexpCapture(regexp, 3) and
144+
key = script.regexpCapture(regexp, 4) and
145+
value = trimQuotes(script.regexpCapture(regexp, 5))
146+
or
147+
regexp = "(?i)(echo|printf|write-output)\\s*(['|\"])?::(add-[a-z]+)\\s*::(.*)" and
148+
cmd = script.regexpCapture(regexp, 3) and
149+
key = "" and
150+
value = trimQuotes(script.regexpCapture(regexp, 4))
151+
)
152+
}
153+
154+
bindingset[script]
155+
predicate heredocFileWrite(string script, string cmd, string file, string content, string filters) {
156+
exists(string regexp |
157+
regexp =
158+
"(?msi).*^(cat)\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*<<\\s*['\"]?(\\S+)['\"]?\\s*\n(.*?)\n\\4\\s*$.*" and
159+
cmd = script.regexpCapture(regexp, 1) and
160+
file = trimQuotes(script.regexpCapture(regexp, 4)) and
161+
content = script.regexpCapture(regexp, 6) and
162+
filters = ""
163+
or
164+
regexp =
165+
"(?msi).*^(cat)\\s*(<<|<)\\s*[-]?['\"]?(\\S+)['\"]?\\s*([^>]*)(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+)\\s*\n(.*?)\n\\3\\s*$.*" and
166+
cmd = script.regexpCapture(regexp, 1) and
167+
file = trimQuotes(script.regexpCapture(regexp, 7)) and
168+
filters = script.regexpCapture(regexp, 4) and
169+
content = script.regexpCapture(regexp, 8)
170+
)
171+
}
172+
173+
bindingset[script]
174+
predicate linesFileWrite(string script, string cmd, string file, string content, string filters) {
175+
exists(string regexp, string var_name |
176+
regexp =
177+
"(?msi).*((echo|printf)\\s+['|\"]?(.*?<<(\\S+))['|\"]?\\s*>>\\s*(\\S+)\\s*[\r\n]+)" +
178+
"(((.*?)\\s*>>\\s*\\S+\\s*[\r\n]+)+)" +
179+
"((echo|printf)\\s+['|\"]?(EOF)['|\"]?\\s*>>\\s*\\S+\\s*[\r\n]*).*" and
180+
var_name = trimQuotes(script.regexpCapture(regexp, 3)).regexpReplaceAll("<<\\s*(\\S+)", "") and
181+
content =
182+
var_name + "=$(" +
183+
trimQuotes(script.regexpCapture(regexp, 6))
184+
.regexpReplaceAll(">>.*GITHUB_(ENV|OUTPUT)(})?", "")
185+
.trim() + ")" and
186+
cmd = "echo" and
187+
file = trimQuotes(script.regexpCapture(regexp, 5)) and
188+
filters = ""
189+
)
190+
}
191+
192+
bindingset[script]
193+
predicate blockFileWrite(string script, string cmd, string file, string content, string filters) {
194+
exists(string regexp, string first_line, string var_name |
195+
regexp =
196+
"(?msi).*^\\s*\\{\\s*[\r\n]" +
197+
//
198+
"(.*?)" +
199+
//
200+
"(\\s*\\}\\s*(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*(\\S+))\\s*$.*" and
201+
first_line = script.regexpCapture(regexp, 1).splitAt("\n", 0).trim() and
202+
var_name = first_line.regexpCapture("echo\\s+('|\\\")?(.*)<<.*", 2) and
203+
content = var_name + "=$(" + script.regexpCapture(regexp, 1).splitAt("\n").trim() + ")" and
204+
not content.indexOf("EOF") > 0 and
205+
file = trimQuotes(script.regexpCapture(regexp, 5)) and
206+
cmd = "echo" and
207+
filters = ""
208+
)
209+
}
210+
211+
bindingset[script]
212+
predicate multiLineFileWrite(
213+
string script, string cmd, string file, string content, string filters
214+
) {
215+
heredocFileWrite(script, cmd, file, content, filters)
216+
or
217+
linesFileWrite(script, cmd, file, content, filters)
218+
or
219+
blockFileWrite(script, cmd, file, content, filters)
220+
}
221+
222+
bindingset[script, file_var]
223+
predicate extractFileWrite(string script, string file_var, string content) {
224+
// single line assignment
225+
exists(string file_expr, string raw_content |
226+
isParameterExpansion(file_expr, file_var, _, _) and
227+
singleLineFileWrite(script.splitAt("\n"), _, file_expr, raw_content, _) and
228+
content = trimQuotes(raw_content)
229+
)
230+
or
231+
// workflow command assignment
232+
exists(string key, string value, string cmd |
233+
(
234+
file_var = "GITHUB_ENV" and
235+
cmd = "set-env" and
236+
content = key + "=" + value
237+
or
238+
file_var = "GITHUB_OUTPUT" and
239+
cmd = "set-output" and
240+
content = key + "=" + value
241+
or
242+
file_var = "GITHUB_PATH" and
243+
cmd = "add-path" and
244+
content = value
245+
) and
246+
singleLineWorkflowCmd(script.splitAt("\n"), cmd, key, value)
247+
)
248+
or
249+
// multiline assignment
250+
exists(string file_expr, string raw_content |
251+
multiLineFileWrite(script, _, file_expr, raw_content, _) and
252+
isParameterExpansion(file_expr, file_var, _, _) and
253+
content = trimQuotes(raw_content)
254+
)
255+
}
256+
257+
/** Writes the content of the file specified by `path` into a file pointed to by `file_var` */
258+
predicate fileToFileWrite(Run run, string file_var, string path) {
259+
exists(string regexp, string stmt, string file_expr |
260+
regexp =
261+
"(?i)(cat)\\s*" + "((?:(?!<<|<<-)[^>\n])+)\\s*" +
262+
"(>>|>|\\s*\\|\\s*tee\\s*(-a|--append)?)\\s*" + "(\\S+)" and
263+
stmt = run.getAStmt() and
264+
file_expr = trimQuotes(stmt.regexpCapture(regexp, 5)) and
265+
path = stmt.regexpCapture(regexp, 2) and
266+
containsParameterExpansion(file_expr, file_var, _, _)
267+
)
268+
}
269+
270+
predicate fileToGitHubEnv(Run run, string path) { fileToFileWrite(run, "GITHUB_ENV", path) }
271+
272+
predicate fileToGitHubOutput(Run run, string path) { fileToFileWrite(run, "GITHUB_OUTPUT", path) }
273+
274+
predicate fileToGitHubPath(Run run, string path) { fileToFileWrite(run, "GITHUB_PATH", path) }
275+
276+
bindingset[snippet]
277+
predicate outputsPartialFileContent(Run run, string snippet) {
278+
// e.g.
279+
// echo FOO=`yq '.foo' foo.yml` >> $GITHUB_ENV
280+
// echo "FOO=$(<foo.txt)" >> $GITHUB_ENV
281+
// yq '.foo' foo.yml >> $GITHUB_PATH
282+
// cat foo.txt >> $GITHUB_PATH
283+
exists(int i, string line, string cmd |
284+
run.getStmt(i) = line and
285+
line.indexOf(snippet.regexpReplaceAll("^\\$\\(", "").regexpReplaceAll("\\)$", "")) > -1 and
286+
run.getCommand(i) = cmd and
287+
cmd.indexOf(["<", Bash::partialFileContentCommand() + " "]) = 0
288+
)
289+
}
290+
291+
/**
292+
* Holds if the Run scripts contains an access to an environment variable called `var`
293+
* which value may get appended to the GITHUB_XXX special file
294+
*/
295+
predicate envReachingGitHubFileWrite(Run run, string var, string file_var, string field) {
296+
exists(string file_write_value |
297+
(
298+
file_var = "GITHUB_ENV" and
299+
run.getAWriteToGitHubEnv(field, file_write_value)
300+
or
301+
file_var = "GITHUB_OUTPUT" and
302+
run.getAWriteToGitHubOutput(field, file_write_value)
303+
or
304+
file_var = "GITHUB_PATH" and
305+
field = "PATH" and
306+
run.getAWriteToGitHubPath(file_write_value)
307+
) and
308+
envReachingRunExpr(run, var, file_write_value)
309+
)
310+
}
311+
312+
/**
313+
* Holds if and environment variable is used, directly or indirectly, in a Run's step expression.
314+
* Where the expression is a string captured from the Run's script.
315+
*/
316+
bindingset[expr]
317+
predicate envReachingRunExpr(Run run, string var, string expr) {
318+
exists(string var2, string value2 |
319+
// VAR2=${VAR:-default} (var2=value2)
320+
// echo "FIELD=${VAR2:-default}" >> $GITHUB_ENV (field, file_write_value)
321+
run.getAnAssignment(var2, value2) and
322+
containsParameterExpansion(value2, var, _, _) and
323+
containsParameterExpansion(expr, var2, _, _)
324+
)
325+
or
326+
// var reaches the file write directly
327+
// echo "FIELD=${VAR:-default}" >> $GITHUB_ENV (field, file_write_value)
328+
containsParameterExpansion(expr, var, _, _)
329+
}
330+
331+
/**
332+
* Holds if the Run scripts contains a command substitution (`cmd`)
333+
* which output may get appended to the GITHUB_XXX special file
334+
*/
335+
predicate cmdReachingGitHubFileWrite(Run run, string cmd, string file_var, string field) {
336+
exists(string file_write_value |
337+
(
338+
file_var = "GITHUB_ENV" and
339+
run.getAWriteToGitHubEnv(field, file_write_value)
340+
or
341+
file_var = "GITHUB_OUTPUT" and
342+
run.getAWriteToGitHubOutput(field, file_write_value)
343+
or
344+
file_var = "GITHUB_PATH" and
345+
field = "PATH" and
346+
run.getAWriteToGitHubPath(file_write_value)
347+
) and
348+
(
349+
// cmd output is assigned to a second variable (var2) and var2 reaches the file write
350+
exists(string var2, string value2 |
351+
// VAR2=$(cmd)
352+
// echo "FIELD=${VAR2:-default}" >> $GITHUB_ENV (field, file_write_value)
353+
run.getAnAssignment(var2, value2) and
354+
containsCmdSubstitution(value2, cmd) and
355+
containsParameterExpansion(file_write_value, var2, _, _)
356+
)
357+
or
358+
// var reaches the file write directly
359+
// echo "FIELD=$(cmd)" >> $GITHUB_ENV (field, file_write_value)
360+
containsCmdSubstitution(file_write_value, cmd)
361+
)
362+
)
363+
}
364+
}

0 commit comments

Comments
 (0)