Skip to content

Commit bbe63e9

Browse files
authored
Merge pull request #459 from IlyaGulya/bugfix/false-positive-script-injection
Fix false-positive script injection warnings when using builtin functions with stable outputs
2 parents 213a455 + a950ac3 commit bbe63e9

File tree

5 files changed

+219
-38
lines changed

5 files changed

+219
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
/playground-dist
1414
/actionlint-workflow-ast
1515
/.git-hooks/.timestamp
16+
/.idea

expr_ast.go

Lines changed: 115 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,83 +5,121 @@ package actionlint
55
type ExprNode interface {
66
// Token returns the first token of the node. This method is useful to get position of this node.
77
Token() *Token
8+
// Parent returns the parent node of this node.
9+
Parent() ExprNode
810
}
911

1012
// Variable
1113

1214
// VariableNode is node for variable access.
1315
type VariableNode struct {
1416
// Name is name of the variable
15-
Name string
16-
tok *Token
17+
Name string
18+
tok *Token
19+
parent ExprNode
1720
}
1821

1922
// Token returns the first token of the node. This method is useful to get position of this node.
2023
func (n *VariableNode) Token() *Token {
2124
return n.tok
2225
}
2326

27+
// Parent returns the parent node of this node.
28+
func (n *VariableNode) Parent() ExprNode {
29+
return n.parent
30+
}
31+
2432
// Literals
2533

2634
// NullNode is node for null literal.
2735
type NullNode struct {
28-
tok *Token
36+
tok *Token
37+
parent ExprNode
2938
}
3039

3140
// Token returns the first token of the node. This method is useful to get position of this node.
3241
func (n *NullNode) Token() *Token {
3342
return n.tok
3443
}
3544

45+
// Parent returns the parent node of this node.
46+
func (n *NullNode) Parent() ExprNode {
47+
return n.parent
48+
}
49+
3650
// BoolNode is node for boolean literal, true or false.
3751
type BoolNode struct {
3852
// Value is value of the boolean literal.
39-
Value bool
40-
tok *Token
53+
Value bool
54+
tok *Token
55+
parent ExprNode
4156
}
4257

4358
// Token returns the first token of the node. This method is useful to get position of this node.
4459
func (n *BoolNode) Token() *Token {
4560
return n.tok
4661
}
4762

63+
// Parent returns the parent node of this node.
64+
func (n *BoolNode) Parent() ExprNode {
65+
return n.parent
66+
}
67+
4868
// IntNode is node for integer literal.
4969
type IntNode struct {
5070
// Value is value of the integer literal.
51-
Value int
52-
tok *Token
71+
Value int
72+
tok *Token
73+
parent ExprNode
5374
}
5475

5576
// Token returns the first token of the node. This method is useful to get position of this node.
5677
func (n *IntNode) Token() *Token {
5778
return n.tok
5879
}
5980

81+
// Parent returns the parent node of this node.
82+
func (n *IntNode) Parent() ExprNode {
83+
return n.parent
84+
}
85+
6086
// FloatNode is node for float literal.
6187
type FloatNode struct {
6288
// Value is value of the float literal.
63-
Value float64
64-
tok *Token
89+
Value float64
90+
tok *Token
91+
parent ExprNode
6592
}
6693

6794
// Token returns the first token of the node. This method is useful to get position of this node.
6895
func (n *FloatNode) Token() *Token {
6996
return n.tok
7097
}
7198

99+
// Parent returns the parent node of this node.
100+
func (n *FloatNode) Parent() ExprNode {
101+
return n.parent
102+
}
103+
72104
// StringNode is node for string literal.
73105
type StringNode struct {
74106
// Value is value of the string literal. Escapes are resolved and quotes at both edges are
75107
// removed.
76-
Value string
77-
tok *Token
108+
Value string
109+
tok *Token
110+
parent ExprNode
78111
}
79112

80113
// Token returns the first token of the node. This method is useful to get position of this node.
81114
func (n *StringNode) Token() *Token {
82115
return n.tok
83116
}
84117

118+
// Parent returns the parent node of this node.
119+
func (n *StringNode) Parent() ExprNode {
120+
return n.parent
121+
}
122+
85123
// Operators
86124

87125
// ObjectDerefNode represents property dereference of object like 'foo.bar'.
@@ -90,52 +128,76 @@ type ObjectDerefNode struct {
90128
Receiver ExprNode
91129
// Property is a name of property to access.
92130
Property string
131+
parent ExprNode
93132
}
94133

95134
// Token returns the first token of the node. This method is useful to get position of this node.
96-
func (n ObjectDerefNode) Token() *Token {
135+
func (n *ObjectDerefNode) Token() *Token {
97136
return n.Receiver.Token()
98137
}
99138

139+
// Parent returns the parent node of this node.
140+
func (n *ObjectDerefNode) Parent() ExprNode {
141+
return n.parent
142+
}
143+
100144
// ArrayDerefNode represents elements dereference of arrays like '*' in 'foo.bar.*.piyo'.
101145
type ArrayDerefNode struct {
102146
// Receiver is an expression at receiver of array element dereference.
103147
Receiver ExprNode
148+
parent ExprNode
104149
}
105150

106151
// Token returns the first token of the node. This method is useful to get position of this node.
107-
func (n ArrayDerefNode) Token() *Token {
152+
func (n *ArrayDerefNode) Token() *Token {
108153
return n.Receiver.Token()
109154
}
110155

156+
// Parent returns the parent node of this node.
157+
func (n *ArrayDerefNode) Parent() ExprNode {
158+
return n.parent
159+
}
160+
111161
// IndexAccessNode is node for index access, which represents dynamic object property access or
112162
// array index access.
113163
type IndexAccessNode struct {
114164
// Operand is an expression at operand of index access, which should be array or object.
115165
Operand ExprNode
116166
// Index is an expression at index, which should be integer or string.
117-
Index ExprNode
167+
Index ExprNode
168+
parent ExprNode
118169
}
119170

120171
// Token returns the first token of the node. This method is useful to get position of this node.
121172
func (n *IndexAccessNode) Token() *Token {
122173
return n.Operand.Token()
123174
}
124175

176+
// Parent returns the parent node of this node.
177+
func (n *IndexAccessNode) Parent() ExprNode {
178+
return n.parent
179+
}
180+
125181
// Note: Currently only ! is a logical unary operator
126182

127183
// NotOpNode is node for unary ! operator.
128184
type NotOpNode struct {
129185
// Operand is an expression at operand of ! operator.
130186
Operand ExprNode
131187
tok *Token
188+
parent ExprNode
132189
}
133190

134191
// Token returns the first token of the node. This method is useful to get position of this node.
135192
func (n *NotOpNode) Token() *Token {
136193
return n.tok
137194
}
138195

196+
// Parent returns the parent node of this node.
197+
func (n *NotOpNode) Parent() ExprNode {
198+
return n.parent
199+
}
200+
139201
// CompareOpNodeKind is a kind of compare operators; ==, !=, <, <=, >, >=.
140202
type CompareOpNodeKind int
141203

@@ -187,14 +249,20 @@ type CompareOpNode struct {
187249
// Left is an expression for left hand side of the binary operator.
188250
Left ExprNode
189251
// Right is an expression for right hand side of the binary operator.
190-
Right ExprNode
252+
Right ExprNode
253+
parent ExprNode
191254
}
192255

193256
// Token returns the first token of the node. This method is useful to get position of this node.
194257
func (n *CompareOpNode) Token() *Token {
195258
return n.Left.Token()
196259
}
197260

261+
// Parent returns the parent node of this node.
262+
func (n *CompareOpNode) Parent() ExprNode {
263+
return n.parent
264+
}
265+
198266
// LogicalOpNodeKind is a kind of logical operators; && and ||.
199267
type LogicalOpNodeKind int
200268

@@ -225,30 +293,42 @@ type LogicalOpNode struct {
225293
// Left is an expression for left hand side of the binary operator.
226294
Left ExprNode
227295
// Right is an expression for right hand side of the binary operator.
228-
Right ExprNode
296+
Right ExprNode
297+
parent ExprNode
229298
}
230299

231300
// Token returns the first token of the node. This method is useful to get position of this node.
232301
func (n *LogicalOpNode) Token() *Token {
233302
return n.Left.Token()
234303
}
235304

305+
// Parent returns the parent node of this node.
306+
func (n *LogicalOpNode) Parent() ExprNode {
307+
return n.parent
308+
}
309+
236310
// FuncCallNode represents function call in expression.
237311
// Note that currently only calling builtin functions is supported.
238312
type FuncCallNode struct {
239313
// Callee is a name of called function. This is string value because currently only built-in
240314
// functions can be called.
241315
Callee string
242316
// Args is arguments of the function call.
243-
Args []ExprNode
244-
tok *Token
317+
Args []ExprNode
318+
tok *Token
319+
parent ExprNode
245320
}
246321

247322
// Token returns the first token of the node. This method is useful to get position of this node.
248323
func (n *FuncCallNode) Token() *Token {
249324
return n.tok
250325
}
251326

327+
// Parent returns the parent node of this node.
328+
func (n *FuncCallNode) Parent() ExprNode {
329+
return n.parent
330+
}
331+
252332
// VisitExprNodeFunc is a visitor function for VisitExprNode(). The entering argument is set to
253333
// true when it is called before visiting children. It is set to false when it is called after
254334
// visiting children. It means that this function is called twice for the same node. The parent
@@ -275,8 +355,8 @@ func visitExprNode(n, p ExprNode, f VisitExprNodeFunc) {
275355
visitExprNode(n.Left, n, f)
276356
visitExprNode(n.Right, n, f)
277357
case *FuncCallNode:
278-
for _, a := range n.Args {
279-
visitExprNode(a, n, f)
358+
for i := range n.Args {
359+
visitExprNode(n.Args[i], n, f)
280360
}
281361
}
282362
f(n, p, false)
@@ -286,3 +366,18 @@ func visitExprNode(n, p ExprNode, f VisitExprNodeFunc) {
286366
func VisitExprNode(n ExprNode, f VisitExprNodeFunc) {
287367
visitExprNode(n, nil, f)
288368
}
369+
370+
// FindParent applies predicate to each parent of this node until predicate returns true.
371+
// Then it returns result of predicate. If no parent found, returns nil, false.
372+
func FindParent[T ExprNode](n ExprNode, predicate func(n ExprNode) (T, bool)) (T, bool) {
373+
parent := n.Parent()
374+
for parent != nil {
375+
t, ok := predicate(parent)
376+
if ok {
377+
return t, true
378+
}
379+
parent = parent.Parent()
380+
}
381+
var zero T
382+
return zero, false
383+
}

expr_insecure.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ var BuiltinUntrustedInputs = UntrustedInputSearchRoots{
135135
),
136136
}
137137

138+
// Contains functions which are considered to be safe.
139+
// No script injection is possible if unsafe expression is used inside these functions
140+
// because they have stable determined output
141+
var safeFunctions = map[string]bool{
142+
"contains": true,
143+
"startswith": true,
144+
"endswith": true,
145+
}
146+
138147
// UntrustedInputChecker is a checker to detect untrusted inputs in an expression syntax tree.
139148
// This checker checks object property accesses, array index accesses, and object filters. And
140149
// detects paths to untrusted inputs. Found errors are stored in this instance and can be get via
@@ -292,8 +301,25 @@ func (u *UntrustedInputChecker) end() {
292301
u.reset()
293302
}
294303

304+
// IsFunctionSafe Checks if this function is safe in terms of script injection possibility when using unsafe args
305+
func (u *UntrustedInputChecker) IsFunctionSafe(name string) bool {
306+
return safeFunctions[strings.ToLower(name)]
307+
}
308+
295309
// OnVisitNodeLeave is a callback which should be called on visiting node after visiting its children.
296310
func (u *UntrustedInputChecker) OnVisitNodeLeave(n ExprNode) {
311+
_, isInsideSafeFunctionCall := FindParent(n, func(n ExprNode) (*FuncCallNode, bool) {
312+
if funcCall, ok := n.(*FuncCallNode); ok && u.IsFunctionSafe(funcCall.Callee) {
313+
return funcCall, true
314+
}
315+
return nil, false
316+
})
317+
318+
// Skip unsafe checks if we are inside of safe function call expression
319+
if isInsideSafeFunctionCall {
320+
return
321+
}
322+
297323
switch n := n.(type) {
298324
case *VariableNode:
299325
u.end()

expr_insecure_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,14 @@ func TestExprInsecureDetectUntrustedValue(t *testing.T) {
193193
},
194194
},
195195
testCase{
196-
"contains(github.event.pages.*.page_name, github.event.issue.title)",
196+
"fromJson(github.event.pages.*.page_name, github.event.issue.title)",
197197
[]string{
198198
"github.event.pages.*.page_name",
199199
"github.event.issue.title",
200200
},
201201
},
202202
testCase{
203-
"contains(github.event.*.body, github.event.*.*)",
203+
"fromJson(github.event.*.body, github.event.*.*)",
204204
[]string{
205205
"github.event.",
206206
"github.event.",
@@ -310,6 +310,14 @@ func TestExprInsecureNoUntrustedValue(t *testing.T) {
310310
"matrix.foo[github.event.pages].page_name",
311311
"github.event.issue.body.foo.bar",
312312
"github.event.issue.body[0]",
313+
314+
"contains(github.event.issue.title, 'bug')",
315+
"contains(github.event.issue.body, github.event.issue.title)",
316+
"startsWith(github.event.comment.body, 'LGTM')",
317+
"endsWith(github.event.pull_request.title, github.event.issue.title)",
318+
"contains(github.event.*.body, 'sensitive')",
319+
"startsWith(github.event.*.body[0], 'Important')",
320+
"endsWith(github.event.commits[0].message, github.event.pull_request.title)",
313321
}
314322

315323
for _, input := range inputs {

0 commit comments

Comments
 (0)