Skip to content

Commit b49cd3b

Browse files
author
Alvaro Muñoz
committed
Better handling of EnvVar Injection and Argument Injection
1 parent e2e1ddd commit b49cd3b

17 files changed

Lines changed: 246 additions & 172 deletions

ql/lib/codeql/actions/Bash.qll

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,13 @@ class BashShellScript extends ShellScript {
220220
override string getCommand(int i) {
221221
// remove redirection
222222
result =
223-
this.getCmd(i).regexpReplaceAll("(>|>>|2>|2>>|<|<<<)\\s*[\\{\\}\\$\"'_\\-0-9a-zA-Z]+$", "") and
223+
this.getCmd(i)
224+
.regexpReplaceAll("(>|>>|2>|2>>|<|<<<)\\s*[\\{\\}\\$\"'_\\-0-9a-zA-Z]+$", "")
225+
.trim() and
224226
// exclude variable declarations
225227
not result.regexpMatch("^[a-zA-Z0-9\\-_]+=") and
228+
// exclude comments
229+
not result.trim().indexOf("#") = 0 and
226230
// exclude the following keywords
227231
not result =
228232
[
@@ -359,11 +363,11 @@ module Bash {
359363
exists(string regexp |
360364
// $(cmd)
361365
regexp = ".*\\$\\(([^)]+)\\).*" and
362-
cmd = expr.regexpCapture(regexp, 1)
366+
cmd = expr.regexpCapture(regexp, 1).trim()
363367
or
364368
// `cmd`
365369
regexp = ".*`([^`]+)`.*" and
366-
cmd = expr.regexpCapture(regexp, 1)
370+
cmd = expr.regexpCapture(regexp, 1).trim()
367371
)
368372
}
369373

@@ -657,8 +661,8 @@ module Bash {
657661
exists(string cmd, string regex, int command_group, int argument_group |
658662
cmd = script.getACommand() and
659663
argumentInjectionSinksDataModel(regex, command_group, argument_group) and
660-
argument = cmd.regexpCapture(regex, argument_group) and
661-
command = cmd.regexpCapture(regex, command_group) and
664+
argument = cmd.regexpCapture(regex, argument_group).trim() and
665+
command = cmd.regexpCapture(regex, command_group).trim() and
662666
envReachingRunExpr(script, source, argument)
663667
)
664668
}
@@ -669,8 +673,8 @@ module Bash {
669673
exists(string cmd, string regex, int command_group, int argument_group |
670674
cmd = script.getACommand() and
671675
argumentInjectionSinksDataModel(regex, command_group, argument_group) and
672-
argument = cmd.regexpCapture(regex, argument_group) and
673-
command = cmd.regexpCapture(regex, command_group) and
676+
argument = cmd.regexpCapture(regex, argument_group).trim() and
677+
command = cmd.regexpCapture(regex, command_group).trim() and
674678
cmdReachingRunExpr(script, source, argument)
675679
)
676680
}

ql/lib/codeql/actions/config/Config.qll

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,6 @@ predicate externallyTriggerableEventsDataModel(string event) {
4747

4848
private string commandLauncher() { result = ["", "sudo\\s+", "su\\s+", "xvfb-run\\s+"] }
4949

50-
private string commandPrefixDelimiter() { result = "(^|;|\\$\\(|`|\\||&&|\\|\\|)\\s*" }
51-
52-
private string commandSuffixDelimiter() { result = "\\s*(;|\\||\\)|`|&&|\\|\\||$)" }
53-
5450
/**
5551
* MaD models for poisonable commands
5652
* Fields:
@@ -59,9 +55,7 @@ private string commandSuffixDelimiter() { result = "\\s*(;|\\||\\)|`|&&|\\|\\||$
5955
predicate poisonableCommandsDataModel(string regexp) {
6056
exists(string sub_regexp |
6157
Extensions::poisonableCommandsDataModel(sub_regexp) and
62-
// find regexp
63-
regexp =
64-
commandPrefixDelimiter() + commandLauncher() + sub_regexp + "(.*?)" + commandSuffixDelimiter()
58+
regexp = commandLauncher() + sub_regexp + ".*"
6559
)
6660
}
6761

@@ -74,10 +68,7 @@ predicate poisonableCommandsDataModel(string regexp) {
7468
predicate poisonableLocalScriptsDataModel(string regexp, int command_group) {
7569
exists(string sub_regexp |
7670
Extensions::poisonableLocalScriptsDataModel(sub_regexp, command_group) and
77-
// capture regexp
78-
regexp =
79-
".*" + commandPrefixDelimiter() + commandLauncher() + sub_regexp + commandSuffixDelimiter() +
80-
".*"
71+
regexp = commandLauncher() + sub_regexp + ".*"
8172
)
8273
}
8374

@@ -91,8 +82,7 @@ predicate poisonableLocalScriptsDataModel(string regexp, int command_group) {
9182
predicate argumentInjectionSinksDataModel(string regexp, int command_group, int argument_group) {
9283
exists(string sub_regexp |
9384
Extensions::argumentInjectionSinksDataModel(sub_regexp, command_group, argument_group) and
94-
// capture regexp
95-
regexp = ".*" + commandPrefixDelimiter() + sub_regexp // + commandSuffixDelimiter() + ".*"
85+
regexp = commandLauncher() + sub_regexp
9686
)
9787
}
9888

ql/lib/codeql/actions/dataflow/FlowSources.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ class GitCommandSource extends RemoteFlowSource, CommandSource {
100100
) and
101101
this.asExpr() = run.getScript() and
102102
checkout.getAFollowingStep() = run and
103-
run.getScript().getACommand() = cmd and
103+
run.getScript().getAStmt() = cmd and
104104
cmd.indexOf("git") = 0 and
105105
untrustedGitCommandsDataModel(cmd_regex, flag) and
106-
cmd.regexpMatch(cmd_regex)
106+
cmd.regexpMatch(".*" + cmd_regex + ".*")
107107
)
108108
}
109109

ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import codeql.actions.dataflow.FlowSources
99

1010
abstract class EnvVarInjectionSink extends DataFlow::Node { }
1111

12+
string sanitizerCommand() {
13+
result =
14+
[
15+
"tr\\s+(-d\\s*)?('|\")?.n('|\")?", // tr -d '\n' ' ', tr '\n' ' '
16+
"tr\\s+-cd\\s+.*:alpha:", // tr -cd '[:alpha:_]'
17+
"(head|tail)\\s+-n\\s+1" // head -n 1, tail -n 1
18+
]
19+
}
20+
1221
/**
1322
* Holds if a Run step declares an environment variable with contents from a local file.
14-
* e.g.
15-
* run: |
16-
* cat test-results/.env >> $GITHUB_ENV
17-
*
18-
* echo "sha=$(cat test-results/sha-number)" >> $GITHUB_ENV
19-
* echo "sha=$(<test-results/sha-number)" >> $GITHUB_ENV
20-
*
21-
* FOO=$(cat test-results/sha-number)
22-
* echo "FOO=$FOO" >> $GITHUB_ENV
2323
*/
2424
class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink {
2525
EnvVarInjectionFromFileReadSink() {
@@ -31,11 +31,19 @@ class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink {
3131
this.asExpr() = run.getScript() and
3232
step.getAFollowingStep() = run and
3333
(
34-
exists(string cmd |
35-
run.getScript().getACmdReachingGitHubEnvWrite(cmd, _) and
36-
run.getScript().getAFileReadCommand() = cmd
34+
// eg:
35+
// echo "SHA=$(cat test-results/sha-number)" >> $GITHUB_ENV
36+
// echo "SHA=$(<test-results/sha-number)" >> $GITHUB_ENV
37+
// FOO=$(cat test-results/sha-number)
38+
// echo "FOO=$FOO" >> $GITHUB_ENV
39+
exists(string cmd, string var, string sanitizer |
40+
run.getScript().getAFileReadCommand() = cmd and
41+
run.getScript().getACmdReachingGitHubEnvWrite(cmd, var) and
42+
run.getScript().getACmdReachingGitHubEnvWrite(sanitizer, var) and
43+
not exists(sanitizer.regexpFind(sanitizerCommand(), _, _))
3744
)
3845
or
46+
// eg: cat test-results/.env >> $GITHUB_ENV
3947
run.getScript().fileToGitHubEnv(_)
4048
)
4149
)
@@ -51,9 +59,18 @@ class EnvVarInjectionFromFileReadSink extends EnvVarInjectionSink {
5159
*/
5260
class EnvVarInjectionFromCommandSink extends EnvVarInjectionSink {
5361
EnvVarInjectionFromCommandSink() {
54-
exists(CommandSource source |
62+
exists(CommandSource source, Run run, string var |
5563
this.asExpr() = source.getEnclosingRun().getScript() and
56-
source.getEnclosingRun().getScript().getACmdReachingGitHubEnvWrite(source.getCommand(), _)
64+
run = source.getEnclosingRun() and
65+
run.getScript().getACmdReachingGitHubEnvWrite(source.getCommand(), var) and
66+
(
67+
not run.getScript().getACmdReachingGitHubEnvWrite(_, var)
68+
or
69+
exists(string sanitizer |
70+
run.getScript().getACmdReachingGitHubEnvWrite(sanitizer, var) and
71+
not exists(sanitizer.regexpFind(sanitizerCommand(), _, _))
72+
)
73+
)
5774
)
5875
}
5976
}
@@ -68,10 +85,18 @@ class EnvVarInjectionFromCommandSink extends EnvVarInjectionSink {
6885
*/
6986
class EnvVarInjectionFromEnvVarSink extends EnvVarInjectionSink {
7087
EnvVarInjectionFromEnvVarSink() {
71-
exists(Run run, string var_name |
88+
exists(Run run, string var_name, string var |
7289
exists(run.getInScopeEnvVarExpr(var_name)) and
7390
run.getScript() = this.asExpr() and
74-
run.getScript().getAnEnvReachingGitHubEnvWrite(var_name, _)
91+
run.getScript().getAnEnvReachingGitHubEnvWrite(var_name, var) and
92+
(
93+
not run.getScript().getACmdReachingGitHubEnvWrite(_, var)
94+
or
95+
exists(string sanitizer |
96+
run.getScript().getACmdReachingGitHubEnvWrite(sanitizer, var) and
97+
not exists(sanitizer.regexpFind(sanitizerCommand(), _, _))
98+
)
99+
)
75100
)
76101
}
77102
}

ql/lib/codeql/actions/security/PoisonableSteps.qll

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,15 @@ import codeql.actions.config.Config
33

44
abstract class PoisonableStep extends Step { }
55

6-
private string dangerousActions() {
7-
exists(string action |
8-
poisonableActionsDataModel(action) and
9-
result = action
10-
)
11-
}
12-
136
class DangerousActionUsesStep extends PoisonableStep, UsesStep {
14-
DangerousActionUsesStep() { this.getCallee() = dangerousActions() }
7+
DangerousActionUsesStep() { poisonableActionsDataModel(this.getCallee()) }
158
}
169

1710
class PoisonableCommandStep extends PoisonableStep, Run {
1811
PoisonableCommandStep() {
1912
exists(string regexp |
2013
poisonableCommandsDataModel(regexp) and
21-
this.getScript().getACommand().regexpMatch("^" + regexp + ".*")
14+
this.getScript().getACommand().regexpMatch(regexp)
2215
)
2316
}
2417
}

ql/lib/codeql/actions/security/UntrustedCheckoutQuery.qll

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private module ActionsMutableRefCheckoutConfig implements DataFlow::ConfigSig {
5353
predicate isSink(DataFlow::Node sink) {
5454
exists(Uses uses |
5555
uses.getCallee() = "actions/checkout" and
56-
uses.getArgumentExpr("ref") = sink.asExpr()
56+
uses.getArgumentExpr(["ref", "repository"]) = sink.asExpr()
5757
)
5858
}
5959

@@ -99,7 +99,7 @@ private module ActionsSHACheckoutConfig implements DataFlow::ConfigSig {
9999
predicate isSink(DataFlow::Node sink) {
100100
exists(Uses uses |
101101
uses.getCallee() = "actions/checkout" and
102-
uses.getArgumentExpr("ref") = sink.asExpr()
102+
uses.getArgumentExpr(["ref", "repository"]) = sink.asExpr()
103103
)
104104
}
105105

@@ -199,7 +199,7 @@ class ActionsMutableRefCheckout extends MutableRefCheckoutStep instanceof UsesSt
199199
(
200200
exists(ActionsMutableRefCheckoutFlow::PathNode sink |
201201
ActionsMutableRefCheckoutFlow::flowPath(_, sink) and
202-
sink.getNode().asExpr() = this.getArgumentExpr("ref")
202+
sink.getNode().asExpr() = this.getArgumentExpr(["ref", "repository"])
203203
)
204204
or
205205
// heuristic base on the step id and field name
@@ -243,7 +243,7 @@ class ActionsSHACheckout extends SHACheckoutStep instanceof UsesStep {
243243
(
244244
exists(ActionsSHACheckoutFlow::PathNode sink |
245245
ActionsSHACheckoutFlow::flowPath(_, sink) and
246-
sink.getNode().asExpr() = this.getArgumentExpr("ref")
246+
sink.getNode().asExpr() = this.getArgumentExpr(["ref", "repository"])
247247
)
248248
or
249249
// heuristic base on the step id and field name

ql/lib/ext/config/argument_injection_sinks.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ extensions:
55
# https://gtfobins.github.io/
66
# https://0xn3va.gitbook.io/cheat-sheets/web-application/command-injection/argument-injection
77
data:
8-
- ["(awk)\\s(.*?)", 2, 3]
9-
- ["(curl)\\s(.*?)", 2, 3]
10-
- ["(find)\\s(.*?)", 2, 3]
11-
- ["(git)\\s(.*?)", 2, 3]
12-
- ["(sed)\\s(.*?)", 2, 3]
13-
- ["(tar)\\s(.*?)", 2, 3]
14-
- ["(wget)\\s(.*?)", 2, 3]
15-
- ["(zip)\\s(.*?)", 2, 3]
8+
- ["(awk)\\s(.*?)", 1, 2]
9+
- ["(find)\\s(.*?)", 1, 2]
10+
- ["(git clone)\\s(.*?)", 1, 2]
11+
- ["(sed)\\s(.*?)", 1, 2]
12+
- ["(tar)\\s(.*?)", 1, 2]
13+
- ["(wget)\\s(.*?)", 1, 2]
14+
- ["(zip)\\s(.*?)", 1, 2]
1615

ql/lib/ext/config/poisonable_steps.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ extensions:
6363
extensible: poisonableLocalScriptsDataModel
6464
data:
6565
# TODO: It could also be in the form of `dir/cmd`
66-
- ["(\\.\\/[a-zA-Z0-9\\-_\\./]+)(.*?)", 2]
67-
- ["(\\.\\s+[a-zA-Z0-9\\-_\\./]+)(.*?)", 2] # eg: . venv/bin/activate
68-
- ["(source|sh|bash|zsh|fish)\\s+(.*?)", 3]
69-
- ["(node)\\s+(.*?)(\\.js|\\.ts)(.*?)", 3]
70-
- ["(python)\\s+(.*?)\\.py(.*?)", 3]
71-
- ["(ruby)\\s+(.*?)\\.rb(.*?)", 3]
72-
- ["(go)\\s+(generate|run)\\s+(.*?)\\.go(.*?)", 4]
73-
- ["(dotnet)\\s+(.*?)\\.csproj(.*?)", 3]
66+
- ["(\\.\\/[^\\s]+)\\b", 1] # eg: ./venv/bin/activate
67+
- ["(\\.\\s+[^\\s]+)\\b", 1] # eg: . venv/bin/activate
68+
- ["(source|sh|bash|zsh|fish)\\s+([^\\s]+)\\b", 2]
69+
- ["(node)\\s+([^\\s]+)(\\.js|\\.ts)\\b", 2]
70+
- ["(python)\\s+([^\\s]+)\\.py\\b", 2]
71+
- ["(ruby)\\s+([^\\s]+)\\.rb\\b", 2]
72+
- ["(go)\\s+(generate|run)\\s+([^\\s]+)\\.go\\b", 3]
73+
- ["(dotnet)\\s+([^\\s]+)\\.csproj\\b", 2]
7474

ql/lib/ext/config/untrusted_git_commands.yml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,29 @@ extensions:
44
extensible: untrustedGitCommandsDataModel
55
data:
66
# FILES=$(git diff-tree --no-commit-id --name-only HEAD -r)
7-
- [".*git\\b.*\\bdiff-tree\\b.*", "filename,multiline"]
7+
- ["git\\b.*\\bdiff-tree\\b", "filename,multiline"]
88
# CHANGES=$(git --no-pager diff --name-only $NAME | grep -v -f .droneignore);
99
# CHANGES=$(git diff --name-only)
10-
- [".*git\\b.*\\bdiff\\b.*", "filename,multiline"]
10+
- ["git\\b.*\\bdiff\\b", "filename,multiline"]
1111
# COMMIT_MESSAGE=$(git log --format=%s -n 1)
12-
- [".*git\\b.*\\blog\\b.*%s.*", "text,online"]
12+
- ["git\\b.*\\blog\\b.*%s", "text,online"]
1313
# COMMIT_MESSAGE=$(git log --format=%B -n 1)
14-
- [".*git\\b.*\\blog\\b.*%B.*", "text,multiline"]
14+
- ["git\\b.*\\blog\\b.*%B", "text,multiline"]
1515
# COMMIT_MESSAGE=$(git log --format=oneline)
16-
- [".*git\\b.*\\blog\\b.*oneline.*", "text,oneline"]
16+
- ["git\\b.*\\blog\\b.*oneline", "text,oneline"]
1717
# COMMIT_MESSAGE=$(git show -s --format=%B)
1818
# COMMIT_MESSAGE=$(git show -s --format=%s)
19-
- [".*git\\b.*\\bshow\\b.*-s.*%s.*", "text,oneline"]
20-
- [".*git\\b.*\\bshow\\b.*-s.*%B.*", "text,multiline"]
19+
- ["git\\b.*\\bshow\\b.*-s.*%s", "text,oneline"]
20+
- ["git\\b.*\\bshow\\b.*-s.*%B", "text,multiline"]
2121
# AUTHOR=$(git log -1 --pretty=format:'%an')
22-
- [".*git\\b.*\\blog\\b.*%an.*", "username,oneline"]
22+
- ["git\\b.*\\blog\\b.*%an", "username,oneline"]
2323
# AUTHOR=$(git show -s --pretty=%an)
24-
- [".*git\\b.*\\bshow\\b.*%an.*", "username,oneline"]
24+
- ["git\\b.*\\bshow\\b.*%an", "username,oneline"]
2525
# EMAIL=$(git log -1 --pretty=format:'%ae')
26-
- [".*git\\b.*\\blog\\b.*%ae.*", "email,oneline"]
26+
- ["git\\b.*\\blog\\b.*%ae", "email,oneline"]
2727
# EMAIL=$(git show -s --pretty=%ae)
28-
- [".*git\\b.*\\bshow\\b.*%ae.*", "email,oneline"]
28+
- ["git\\b.*\\bshow\\b.*%ae", "email,oneline"]
2929
# BRANCH=$(git branch --show-current)
30-
- [".*git\\b.*\\bbranch\\b.*\\b--show-current\\b.*", "branch,oneline"]
30+
- ["git\\b.*\\bbranch\\b.*\\b--show-current\\b", "branch,oneline"]
3131
# BRANCH=$(git rev-parse --abbrev-ref HEAD)
32-
- [".*git\\b.*\\brev-parse\\b.*\\b--abbrev-ref\\b.*", "branch,oneline"]
32+
- ["git\\b.*\\brev-parse\\b.*\\b--abbrev-ref\\b", "branch,oneline"]

0 commit comments

Comments
 (0)