Skip to content

Commit 3c5358c

Browse files
author
Alvaro Muñoz
authored
Merge pull request #6 from GitHubSecurityLab/composite_actions
feat: support for composite action's analysis
2 parents 9030cb3 + e9707af commit 3c5358c

File tree

13 files changed

+243
-62
lines changed

13 files changed

+243
-62
lines changed

ql/lib/codeql/actions/Ast.qll

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ class Statement extends AstNode { }
2727
*/
2828
class Expression extends Statement { }
2929

30+
/**
31+
* A composite action
32+
*/
33+
class CompositeActionStmt extends Statement instanceof Actions::CompositeAction {
34+
RunsStmt getRunsStmt() { result = super.getRuns() }
35+
36+
InputsStmt getInputsStmt() { result = this.(YamlMapping).lookup("inputs") }
37+
38+
OutputsStmt getOutputsStmt() { result = this.(YamlMapping).lookup("outputs") }
39+
40+
string getName() { result = this.getLocation().getFile().getRelativePath() }
41+
}
42+
43+
class RunsStmt extends Statement instanceof Actions::Runs {
44+
StepStmt getAStepStmt() { result = super.getSteps().getElementNode(_) }
45+
46+
StepStmt getStepStmt(int i) { result = super.getSteps().getElementNode(i) }
47+
}
48+
3049
/**
3150
* A Github Actions Workflow
3251
*/
@@ -43,67 +62,45 @@ class ReusableWorkflowStmt extends WorkflowStmt {
4362
this.(Actions::Workflow).getOn().getNode("workflow_call") = workflow_call
4463
}
4564

46-
ReusableWorkflowInputsStmt getInputsStmt() {
47-
result = workflow_call.(YamlMapping).lookup("inputs")
48-
}
65+
InputsStmt getInputsStmt() { result = workflow_call.(YamlMapping).lookup("inputs") }
4966

50-
ReusableWorkflowOutputsStmt getOutputsStmt() {
51-
result = workflow_call.(YamlMapping).lookup("outputs")
52-
}
67+
OutputsStmt getOutputsStmt() { result = workflow_call.(YamlMapping).lookup("outputs") }
5368

5469
string getName() { result = this.getLocation().getFile().getRelativePath() }
5570
}
5671

57-
class ReusableWorkflowInputsStmt extends Statement instanceof YamlMapping {
58-
ReusableWorkflowInputsStmt() {
59-
exists(Actions::On on | on.getNode("workflow_call").(YamlMapping).lookup("inputs") = this)
60-
}
72+
class InputsStmt extends Statement instanceof YamlMapping {
73+
YamlMapping parent;
74+
75+
InputsStmt() { parent.lookup("inputs") = this }
6176

6277
/**
63-
* Gets a specific parameter expression (YamlMapping) by name.
64-
* eg:
65-
* on:
66-
* workflow_call:
67-
* inputs:
68-
* config-path:
69-
* required: true
70-
* type: string
71-
* secrets:
72-
* token:
73-
* required: true
78+
* Gets a specific input expression (YamlMapping) by name.
7479
*/
75-
ReusableWorkflowInputExpr getInputExpr(string name) {
80+
InputExpr getInputExpr(string name) {
7681
result.(YamlString).getValue() = name and
7782
this.(YamlMapping).maps(result, _)
7883
}
7984
}
8085

81-
class ReusableWorkflowInputExpr extends Expression instanceof YamlString { }
86+
class OutputsStmt extends Statement instanceof YamlMapping {
87+
YamlMapping parent;
8288

83-
class ReusableWorkflowOutputsStmt extends Statement instanceof YamlMapping {
84-
ReusableWorkflowOutputsStmt() {
85-
exists(Actions::On on | on.getNode("workflow_call").(YamlMapping).lookup("outputs") = this)
86-
}
89+
OutputsStmt() { parent.lookup("outputs") = this }
8790

8891
/**
89-
* Gets a specific parameter expression (YamlMapping) by name.
90-
* eg:
91-
* on:
92-
* workflow_call:
93-
* outputs:
94-
* firstword:
95-
* description: "The first output string"
96-
* value: ${{ jobs.example_job.outputs.output1 }}
97-
* secondword:
98-
* description: "The second output string"
99-
* value: ${{ jobs.example_job.outputs.output2 }}
92+
* Gets a specific output expression (YamlMapping) by name.
10093
*/
101-
ReusableWorkflowOutputExpr getOutputExpr(string name) {
94+
OutputExpr getOutputExpr(string name) {
10295
this.(YamlMapping).lookup(name).(YamlMapping).lookup("value") = result
10396
}
10497
}
10598

106-
class ReusableWorkflowOutputExpr extends Expression instanceof YamlString { }
99+
// TODO: Needs a characteristic predicate otherwise anything is an output expression
100+
class InputExpr extends Expression instanceof YamlString { }
101+
102+
// TODO: Needs a characteristic predicate otherwise anything is an output expression
103+
class OutputExpr extends Expression instanceof YamlString { }
107104

108105
/**
109106
* A Job is a collection of steps that run in an execution environment.
@@ -369,7 +366,7 @@ class StepOutputAccessExpr extends ExprAccessExpr {
369366
}
370367

371368
override Expression getRefExpr() {
372-
this.getJobStmt() = result.(StepStmt).getJobStmt() and
369+
this.getLocation().getFile() = result.getLocation().getFile() and
373370
result.(StepStmt).getId() = stepId
374371
}
375372
}
@@ -413,10 +410,10 @@ class JobOutputAccessExpr extends ExprAccessExpr {
413410
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
414411
* e.g. `${{ inputs.foo }}`
415412
*/
416-
class ReusableWorkflowInputAccessExpr extends ExprAccessExpr {
413+
class InputAccessExpr extends ExprAccessExpr {
417414
string paramName;
418415

419-
ReusableWorkflowInputAccessExpr() {
416+
InputAccessExpr() {
420417
paramName = this.getExpression().regexpCapture("inputs\\.([A-Za-z0-9_-]+)", 1)
421418
}
422419

@@ -425,6 +422,11 @@ class ReusableWorkflowInputAccessExpr extends ExprAccessExpr {
425422
w.getLocation().getFile() = this.getLocation().getFile() and
426423
w.getInputsStmt().getInputExpr(paramName) = result
427424
)
425+
or
426+
exists(CompositeActionStmt a |
427+
a.getLocation().getFile() = this.getLocation().getFile() and
428+
a.getInputsStmt().getInputExpr(paramName) = result
429+
)
428430
}
429431
}
430432

ql/lib/codeql/actions/controlflow/internal/Cfg.qll

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ module CfgScope {
8888
abstract class CfgScope extends AstNode { }
8989

9090
class WorkflowScope extends CfgScope instanceof WorkflowStmt { }
91+
92+
class CompositeActionScope extends CfgScope instanceof CompositeActionStmt { }
9193
}
9294

9395
private module Implementation implements CfgShared::InputSig<Location> {
@@ -120,9 +122,15 @@ private module Implementation implements CfgShared::InputSig<Location> {
120122

121123
int maxSplits() { result = 0 }
122124

123-
predicate scopeFirst(CfgScope scope, AstNode e) { first(scope.(WorkflowStmt), e) }
125+
predicate scopeFirst(CfgScope scope, AstNode e) {
126+
first(scope.(WorkflowStmt), e) or
127+
first(scope.(CompositeActionStmt), e)
128+
}
124129

125-
predicate scopeLast(CfgScope scope, AstNode e, Completion c) { last(scope.(WorkflowStmt), e, c) }
130+
predicate scopeLast(CfgScope scope, AstNode e, Completion c) {
131+
last(scope.(WorkflowStmt), e, c) or
132+
last(scope.(CompositeActionStmt), e, c)
133+
}
126134

127135
predicate successorTypeIsSimple(SuccessorType t) { t instanceof NormalSuccessor }
128136

@@ -139,6 +147,28 @@ private import CfgImpl
139147
private import Completion
140148
private import CfgScope
141149

150+
private class CompositeActionTree extends StandardPreOrderTree instanceof CompositeActionStmt {
151+
override ControlFlowTree getChildNode(int i) {
152+
result =
153+
rank[i](Expression child, Location l |
154+
(
155+
child = this.(CompositeActionStmt).getInputsStmt() or
156+
child = this.(CompositeActionStmt).getOutputsStmt() or
157+
child = this.(CompositeActionStmt).getRunsStmt()
158+
) and
159+
l = child.getLocation()
160+
|
161+
child
162+
order by
163+
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
164+
)
165+
}
166+
}
167+
168+
private class RunsTree extends StandardPreOrderTree instanceof RunsStmt {
169+
override ControlFlowTree getChildNode(int i) { result = super.getStepStmt(i) }
170+
}
171+
142172
private class WorkflowTree extends StandardPreOrderTree instanceof WorkflowStmt {
143173
override ControlFlowTree getChildNode(int i) {
144174
if this instanceof ReusableWorkflowStmt
@@ -169,8 +199,7 @@ private class WorkflowTree extends StandardPreOrderTree instanceof WorkflowStmt
169199
}
170200
}
171201

172-
private class ReusableWorkflowInputsTree extends StandardPreOrderTree instanceof ReusableWorkflowInputsStmt
173-
{
202+
private class InputsTree extends StandardPreOrderTree instanceof InputsStmt {
174203
override ControlFlowTree getChildNode(int i) {
175204
result =
176205
rank[i](Expression child, Location l |
@@ -183,10 +212,9 @@ private class ReusableWorkflowInputsTree extends StandardPreOrderTree instanceof
183212
}
184213
}
185214

186-
private class InputExprTree extends LeafTree instanceof ReusableWorkflowInputExpr { }
215+
private class InputExprTree extends LeafTree instanceof InputExpr { }
187216

188-
private class ReusableWorkflowOutputsTree extends StandardPreOrderTree instanceof ReusableWorkflowOutputsStmt
189-
{
217+
private class OutputsTree extends StandardPreOrderTree instanceof OutputsStmt {
190218
override ControlFlowTree getChildNode(int i) {
191219
result =
192220
rank[i](Expression child, Location l |
@@ -199,7 +227,7 @@ private class ReusableWorkflowOutputsTree extends StandardPreOrderTree instanceo
199227
}
200228
}
201229

202-
private class OutputExprTree extends LeafTree instanceof ReusableWorkflowOutputExpr { }
230+
private class OutputExprTree extends LeafTree instanceof OutputExpr { }
203231

204232
private class JobTree extends StandardPreOrderTree instanceof JobStmt {
205233
override ControlFlowTree getChildNode(int i) {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,14 @@ private class ExternallyDefinedSource extends RemoteFlowSource {
161161

162162
override string getSourceType() { result = soutceType }
163163
}
164+
165+
/**
166+
* Composite action input sources
167+
*/
168+
private class CompositeActionInputSource extends RemoteFlowSource {
169+
CompositeActionStmt c;
170+
171+
CompositeActionInputSource() { c.getInputsStmt().getInputExpr(_) = this.asExpr() }
172+
173+
override string getSourceType() { result = "Composite action input" }
174+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ predicate runEnvToScriptstep(DataFlow::Node pred, DataFlow::Node succ) {
8383
line = script.splitAt("\n") and
8484
(
8585
line.regexpMatch(".*::set-output\\s+name.*") or
86-
line.regexpMatch(".*>>\\s*$GITHUB_ENV.*")
86+
line.regexpMatch(".*>>\\s*\\$GITHUB_OUTPUT.*")
8787
) and
8888
script.indexOf("$" + ["", "{", "ENV{"] + varName) > 0
8989
) and

ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ class DataFlowCallable instanceof Cfg::CfgScope {
8282
string getName() {
8383
if this instanceof ReusableWorkflowStmt
8484
then result = this.(ReusableWorkflowStmt).getName()
85-
else none()
85+
else
86+
if this instanceof CompositeActionStmt
87+
then result = this.(CompositeActionStmt).getName()
88+
else none()
8689
}
8790
}
8891

@@ -190,11 +193,11 @@ predicate jobsCtxLocalStep(Node nodeFrom, Node nodeTo) {
190193
}
191194

192195
/**
193-
* Holds if there is a local flow step between a ${{}} expression accesing a reusable workflow input variable and the input itself
196+
* Holds if there is a local flow step between a ${{}} expression accesing an input variable and the input itself
194197
* e.g. ${{ inputs.foo }}
195198
*/
196199
predicate inputsCtxLocalStep(Node nodeFrom, Node nodeTo) {
197-
exists(Expression astFrom, ReusableWorkflowInputAccessExpr astTo |
200+
exists(Expression astFrom, InputAccessExpr astTo |
198201
astFrom = nodeFrom.asExpr() and
199202
astTo = nodeTo.asExpr() and
200203
astTo.getRefExpr() = astFrom

ql/lib/codeql/actions/dataflow/internal/DataFlowPublic.qll

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class ExprNode extends Node, TExprNode {
4848
* Reusable workflow input nodes
4949
*/
5050
class ParameterNode extends ExprNode {
51-
private ReusableWorkflowInputExpr parameter;
51+
private InputExpr parameter;
5252

5353
ParameterNode() {
5454
this.asExpr() = parameter and
@@ -63,7 +63,7 @@ class ParameterNode extends ExprNode {
6363

6464
override Location getLocation() { result = parameter.getLocation() }
6565

66-
ReusableWorkflowInputExpr getInputExpr() { result = parameter }
66+
InputExpr getInputExpr() { result = parameter }
6767
}
6868

6969
/**
@@ -87,7 +87,8 @@ class ReturnNode extends ExprNode {
8787

8888
ReturnNode() {
8989
this.getCfgNode() = node and
90-
node.getAstNode() = any(ReusableWorkflowStmt w).getOutputsStmt().getOutputExpr(_)
90+
(node.getAstNode() = any(ReusableWorkflowStmt w).getOutputsStmt().getOutputExpr(_) or
91+
node.getAstNode() = any(CompositeActionStmt a).getOutputsStmt().getOutputExpr(_))
9192
}
9293

9394
ReturnKind getKind() { result = TNormalReturn() }

ql/lib/test/test.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ query predicate parentNodes(AstNode child, AstNode parent) { child.getParentNode
4545

4646
query predicate cfgNodes(Cfg::Node n) {
4747
//any()
48-
n.getAstNode() instanceof ReusableWorkflowOutputsStmt
48+
n.getAstNode() instanceof OutputsStmt
4949
}
5050

5151
query predicate dfNodes(DataFlow::Node e) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @name Composite Action Summaries
3+
* @description Actions that pass user-controlled data to their output variables.
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @security-severity 9.3
7+
* @precision high
8+
* @id actions/composite-action-summaries
9+
* @tags actions
10+
* external/cwe/cwe-020
11+
*/
12+
13+
import actions
14+
import codeql.actions.TaintTracking
15+
import codeql.actions.dataflow.FlowSources
16+
import codeql.actions.dataflow.ExternalFlow
17+
18+
private class OutputVariableSink extends DataFlow::Node {
19+
OutputVariableSink() { exists(OutputsStmt s | s.getOutputExpr(_) = this.asExpr()) }
20+
}
21+
22+
private module MyConfig implements DataFlow::ConfigSig {
23+
predicate isSource(DataFlow::Node source) {
24+
exists(CompositeActionStmt c | c.getInputsStmt().getInputExpr(_) = source.asExpr())
25+
}
26+
27+
predicate isSink(DataFlow::Node sink) { sink instanceof OutputVariableSink }
28+
}
29+
30+
module MyFlow = TaintTracking::Global<MyConfig>;
31+
32+
import MyFlow::PathGraph
33+
34+
from MyFlow::PathNode source, MyFlow::PathNode sink
35+
where MyFlow::flowPath(source, sink)
36+
select sink.getNode(), source, sink, "Summary"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @name Composite Action Sources
3+
* @description Actions that pass user-controlled data to their output variables.
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @security-severity 9.3
7+
* @precision high
8+
* @id actions/composite-action-sources
9+
* @tags actions
10+
* external/cwe/cwe-020
11+
*/
12+
13+
import actions
14+
import codeql.actions.TaintTracking
15+
import codeql.actions.dataflow.FlowSources
16+
import codeql.actions.dataflow.ExternalFlow
17+
18+
private class OutputVariableSink extends DataFlow::Node {
19+
OutputVariableSink() { exists(OutputsStmt s | s.getOutputExpr(_) = this.asExpr()) }
20+
}
21+
22+
private module MyConfig implements DataFlow::ConfigSig {
23+
predicate isSource(DataFlow::Node source) {
24+
source instanceof RemoteFlowSource and
25+
exists(CompositeActionStmt c | c.getAChildNode*() = source.asExpr()) and
26+
not exists(CompositeActionStmt c | c.getInputsStmt().getInputExpr(_) = source.asExpr())
27+
}
28+
29+
predicate isSink(DataFlow::Node sink) { sink instanceof OutputVariableSink }
30+
}
31+
32+
module MyFlow = TaintTracking::Global<MyConfig>;
33+
34+
import MyFlow::PathGraph
35+
36+
from MyFlow::PathNode source, MyFlow::PathNode sink
37+
where MyFlow::flowPath(source, sink)
38+
select sink.getNode(), source, sink, "Source"

0 commit comments

Comments
 (0)