Skip to content

Commit 73878ed

Browse files
author
Alvaro Muñoz
authored
Merge pull request #42 from GitHubSecurityLab/priv_workflows
priv workflows
2 parents 119c7b8 + f7ddd8b commit 73878ed

File tree

14 files changed

+77
-156
lines changed

14 files changed

+77
-156
lines changed

ql/lib/codeql/actions/Ast.qll

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class AstNode instanceof AstNodeImpl {
3535

3636
Workflow getEnclosingWorkflow() { result = super.getEnclosingWorkflow() }
3737

38+
CompositeAction getEnclosingCompositeAction() { result = super.getEnclosingCompositeAction() }
39+
3840
Expression getInScopeEnvVarExpr(string name) { result = super.getInScopeEnvVarExpr(name) }
3941
}
4042

@@ -123,6 +125,25 @@ class Workflow extends AstNode instanceof WorkflowImpl {
123125
Permissions getPermissions() { result = super.getPermissions() }
124126

125127
Strategy getStrategy() { result = super.getStrategy() }
128+
129+
predicate hasSingleTrigger(string trigger) {
130+
this.getATriggerEvent() = trigger and
131+
count(string t | this.getATriggerEvent() = t | t) = 1
132+
}
133+
134+
predicate isPrivileged() {
135+
// The Workflow is triggered by an event other than `pull_request`
136+
not this.hasSingleTrigger("pull_request")
137+
or
138+
// The Workflow is only triggered by `workflow_call` and there is
139+
// a caller workflow triggered by an event other than `pull_request`
140+
this.hasSingleTrigger("workflow_call") and
141+
exists(ExternalJob call, Workflow caller |
142+
call.getCallee() = this.getLocation().getFile().getRelativePath() and
143+
caller = call.getWorkflow() and
144+
not caller.hasSingleTrigger("pull_request")
145+
)
146+
}
126147
}
127148

128149
class ReusableWorkflow extends Workflow instanceof ReusableWorkflowImpl {

ql/lib/codeql/actions/ast/internal/Ast.qll

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,15 @@ abstract class AstNodeImpl extends TAstNode {
9595
JobImpl getEnclosingJob() { result.getAChildNode*() = this.getParentNode() }
9696

9797
/**
98-
* Gets the enclosing workflow statement.
98+
* Gets the enclosing workflow if any.
9999
*/
100100
WorkflowImpl getEnclosingWorkflow() { this = result.getAChildNode*() }
101101

102+
/**
103+
* Gets the enclosing composite action if any.
104+
*/
105+
CompositeActionImpl getEnclosingCompositeAction() { this = result.getAChildNode*() }
106+
102107
/**
103108
* Gets a environment variable expression by name in the scope of the current node.
104109
*/

ql/src/Security/CWE-077/EnvVarInjection.ql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ import codeql.actions.security.EnvVarInjectionQuery
1717
import EnvVarInjectionFlow::PathGraph
1818

1919
from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink
20-
where EnvVarInjectionFlow::flowPath(source, sink)
20+
where
21+
EnvVarInjectionFlow::flowPath(source, sink) and
22+
(
23+
exists(source.getNode().asExpr().getEnclosingCompositeAction())
24+
or
25+
exists(Workflow w |
26+
w = source.getNode().asExpr().getEnclosingWorkflow() and
27+
not w.isPrivileged()
28+
)
29+
)
2130
select sink.getNode(), source, sink,
2231
"Potential environment variable injection in $@, which may be controlled by an external user.",
2332
sink, sink.getNode().asExpr().(Expression).getRawExpression()

ql/src/Security/CWE-077/PrivilegedEnvVarInjection.ql

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@ import actions
1616
import codeql.actions.security.EnvVarInjectionQuery
1717
import EnvVarInjectionFlow::PathGraph
1818

19-
predicate isSingleTriggerWorkflow(Workflow w, string trigger) {
20-
w.getATriggerEvent() = trigger and
21-
count(string t | w.getATriggerEvent() = t | t) = 1
22-
}
23-
24-
from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink, Workflow w
19+
from EnvVarInjectionFlow::PathNode source, EnvVarInjectionFlow::PathNode sink
2520
where
2621
EnvVarInjectionFlow::flowPath(source, sink) and
27-
w = source.getNode().asExpr().getEnclosingWorkflow() and
28-
not isSingleTriggerWorkflow(w, "pull_request")
22+
exists(Workflow w |
23+
w = source.getNode().asExpr().getEnclosingWorkflow() and
24+
w.isPrivileged()
25+
)
2926
select sink.getNode(), source, sink,
3027
"Potential privileged environment variable injection in $@, which may be controlled by an external user.",
3128
sink, sink.getNode().asExpr().(Expression).getRawExpression()

ql/src/Security/CWE-078/CommandInjection.ql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ import codeql.actions.security.CommandInjectionQuery
1717
import CommandInjectionFlow::PathGraph
1818

1919
from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink
20-
where CommandInjectionFlow::flowPath(source, sink)
20+
where
21+
CommandInjectionFlow::flowPath(source, sink) and
22+
(
23+
exists(source.getNode().asExpr().getEnclosingCompositeAction())
24+
or
25+
exists(Workflow w |
26+
w = source.getNode().asExpr().getEnclosingWorkflow() and
27+
not w.isPrivileged()
28+
)
29+
)
2130
select sink.getNode(), source, sink,
2231
"Potential command injection in $@, which may be controlled by an external user.", sink,
2332
sink.getNode().asExpr().(Expression).getRawExpression()

ql/src/Security/CWE-078/PrivilegedCommandInjection.ql

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@ import actions
1616
import codeql.actions.security.CommandInjectionQuery
1717
import CommandInjectionFlow::PathGraph
1818

19-
predicate isSingleTriggerWorkflow(Workflow w, string trigger) {
20-
w.getATriggerEvent() = trigger and
21-
count(string t | w.getATriggerEvent() = t | t) = 1
22-
}
23-
24-
from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink, Workflow w
19+
from CommandInjectionFlow::PathNode source, CommandInjectionFlow::PathNode sink
2520
where
2621
CommandInjectionFlow::flowPath(source, sink) and
27-
w = source.getNode().asExpr().getEnclosingWorkflow() and
28-
not isSingleTriggerWorkflow(w, "pull_request")
22+
exists(Workflow w |
23+
w = source.getNode().asExpr().getEnclosingWorkflow() and
24+
w.isPrivileged()
25+
)
2926
select sink.getNode(), source, sink,
3027
"Potential privileged command injection in $@, which may be controlled by an external user.",
3128
sink, sink.getNode().asExpr().(Expression).getRawExpression()

ql/src/Security/CWE-094/CodeInjection.ql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ import codeql.actions.security.CodeInjectionQuery
1919
import CodeInjectionFlow::PathGraph
2020

2121
from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink
22-
where CodeInjectionFlow::flowPath(source, sink)
22+
where
23+
CodeInjectionFlow::flowPath(source, sink) and
24+
(
25+
exists(source.getNode().asExpr().getEnclosingCompositeAction())
26+
or
27+
exists(Workflow w |
28+
w = source.getNode().asExpr().getEnclosingWorkflow() and
29+
not w.isPrivileged()
30+
)
31+
)
2332
select sink.getNode(), source, sink,
2433
"Potential code injection in $@, which may be controlled by an external user.", sink,
2534
sink.getNode().asExpr().(Expression).getRawExpression()

ql/src/Security/CWE-094/PrivilegedCodeInjection.ql

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,13 @@ import actions
1818
import codeql.actions.security.CodeInjectionQuery
1919
import CodeInjectionFlow::PathGraph
2020

21-
predicate isSingleTriggerWorkflow(Workflow w, string trigger) {
22-
w.getATriggerEvent() = trigger and
23-
count(string t | w.getATriggerEvent() = t | t) = 1
24-
}
25-
26-
from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink, Workflow w
21+
from CodeInjectionFlow::PathNode source, CodeInjectionFlow::PathNode sink
2722
where
2823
CodeInjectionFlow::flowPath(source, sink) and
29-
w = source.getNode().asExpr().getEnclosingWorkflow() and
30-
not isSingleTriggerWorkflow(w, "pull_request")
24+
exists(Workflow w |
25+
w = source.getNode().asExpr().getEnclosingWorkflow() and
26+
w.isPrivileged()
27+
)
3128
select sink.getNode(), source, sink,
3229
"Potential privileged code injection in $@, which may be controlled by an external user.", sink,
3330
sink.getNode().asExpr().(Expression).getRawExpression()

ql/src/Security/CWE-829/ArtifactPoisoning.ql

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,9 @@
1414
import actions
1515
import codeql.actions.security.ArtifactPoisoningQuery
1616

17-
predicate isSingleTriggerWorkflow(Workflow w, string trigger) {
18-
w.getATriggerEvent() = trigger and
19-
count(string t | w.getATriggerEvent() = t | t) = 1
20-
}
21-
22-
from Workflow w, LocalJob job, ArtifactDownloadStep download, Step run
17+
from LocalJob job, ArtifactDownloadStep download, Step run
2318
where
24-
w = job.getWorkflow() and
25-
(
26-
// The Workflow is triggered by an event other than `pull_request`
27-
not isSingleTriggerWorkflow(w, "pull_request")
28-
or
29-
// The Workflow is only triggered by `workflow_call` and there is
30-
// a caller workflow triggered by an event other than `pull_request`
31-
isSingleTriggerWorkflow(w, "workflow_call") and
32-
exists(ExternalJob call, Workflow caller |
33-
call.getCallee() = w.getLocation().getFile().getRelativePath() and
34-
caller = call.getWorkflow() and
35-
not isSingleTriggerWorkflow(caller, "pull_request")
36-
)
37-
) and
19+
job.getWorkflow().isPrivileged() and
3820
(run instanceof Run or run instanceof UsesStep) and
3921
exists(int i, int j |
4022
job.getStep(i) = download and

ql/src/Security/CWE-829/UntrustedCheckout.ql

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -121,26 +121,9 @@ class GitCheckout extends PRHeadCheckoutStep instanceof Run {
121121
}
122122
}
123123

124-
predicate isSingleTriggerWorkflow(Workflow w, string trigger) {
125-
w.getATriggerEvent() = trigger and
126-
count(string t | w.getATriggerEvent() = t | t) = 1
127-
}
128-
129124
from Workflow w, PRHeadCheckoutStep checkout
130125
where
131-
(
132-
// The Workflow is triggered by an event other than `pull_request`
133-
not isSingleTriggerWorkflow(w, "pull_request")
134-
or
135-
// The Workflow is only triggered by `workflow_call` and there is
136-
// a caller workflow triggered by an event other than `pull_request`
137-
isSingleTriggerWorkflow(w, "workflow_call") and
138-
exists(ExternalJob call, Workflow caller |
139-
call.getCallee() = w.getLocation().getFile().getRelativePath() and
140-
caller = call.getWorkflow() and
141-
not isSingleTriggerWorkflow(caller, "pull_request")
142-
)
143-
) and
126+
w.isPrivileged() and
144127
w.getAJob().(LocalJob).getAStep() = checkout and
145128
not exists(ControlCheck check |
146129
checkout.getIf() = check or checkout.getEnclosingJob().getIf() = check

0 commit comments

Comments
 (0)