Skip to content

Commit 0606dff

Browse files
committed
checkers(python): add checkers to detect event tainted system calls with os module
Signed-off-by: Maharshi Basu <basumaharshi10@gmail.com>
1 parent 17d8c32 commit 0606dff

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package python
2+
3+
import (
4+
"slices"
5+
"strings"
6+
7+
sitter "github.com/smacker/go-tree-sitter"
8+
"globstar.dev/analysis"
9+
)
10+
11+
var AwsLambdaDangerousOsSystemCall *analysis.Analyzer = &analysis.Analyzer{
12+
Name: "aws-lambda-dangerous-os-system-call",
13+
Language: analysis.LangPy,
14+
Description: "Detected a potentially dangerous use of the `os` function with an argument influenced by the `event` object. This could allow command injection if external data reaches this call. Use the `subprocess` module instead to prevent security risks.",
15+
Category: analysis.CategorySecurity,
16+
Severity: analysis.SeverityWarning,
17+
Run: checkAwsLambdaDangerousOsSystemCall,
18+
}
19+
20+
func checkAwsLambdaDangerousOsSystemCall(pass *analysis.Pass) (interface{}, error) {
21+
systemCall := []string{"system", "popen", "popen2", "popen3", "popen4"}
22+
eventVarMap := make(map[string]bool) // var for `event['url']` etc.
23+
cmdVarMap := make(map[string]bool) // var for command strings
24+
intermVarMap := make(map[string]bool) // when intermediate variable is used for formatting the command string
25+
26+
// first pass: track the variable names for commands and event subscripts (if called)
27+
analysis.Preorder(pass, func(node *sitter.Node) {
28+
if node.Type() != "assignment" {
29+
return
30+
}
31+
32+
leftNode := node.ChildByFieldName("left")
33+
rightNode := node.ChildByFieldName("right")
34+
35+
if rightNode == nil {
36+
return
37+
}
38+
39+
// f-strings will be added to intermVarMap
40+
if rightNode.Type() == "string" && rightNode.Content(pass.FileContext.Source)[0] != 'f' {
41+
cmdVarMap[leftNode.Content(pass.FileContext.Source)] = true
42+
}
43+
44+
if isEventSubscript(rightNode, pass.FileContext.Source) {
45+
eventVarMap[leftNode.Content(pass.FileContext.Source)] = true
46+
}
47+
})
48+
49+
// second pass: track the variable names of intermediate command string formatting
50+
analysis.Preorder(pass, func(node *sitter.Node) {
51+
if node.Type() != "assignment" {
52+
return
53+
}
54+
55+
leftNode := node.ChildByFieldName("left")
56+
rightNode := node.ChildByFieldName("right")
57+
58+
if rightNode == nil {
59+
return
60+
}
61+
62+
if isTaintedCmdString(rightNode, pass.FileContext.Source, eventVarMap, cmdVarMap, intermVarMap) {
63+
intermVarMap[leftNode.Content(pass.FileContext.Source)] = true
64+
}
65+
})
66+
67+
analysis.Preorder(pass, func(node *sitter.Node) {
68+
if node.Type() != "call" {
69+
return
70+
}
71+
72+
funcNode := node.ChildByFieldName("function")
73+
if funcNode.Type() != "attribute" {
74+
return
75+
}
76+
77+
funcObjNode := funcNode.ChildByFieldName("object")
78+
funcAttrNode := funcNode.ChildByFieldName("attribute")
79+
80+
if funcAttrNode.Type() != "identifier" || funcObjNode.Type() != "identifier" {
81+
return
82+
}
83+
84+
if funcObjNode.Content(pass.FileContext.Source) != "os" {
85+
return
86+
}
87+
88+
if !slices.Contains(systemCall, funcAttrNode.Content(pass.FileContext.Source)) {
89+
return
90+
}
91+
92+
argsListNode := node.ChildByFieldName("arguments")
93+
if argsListNode.Type() != "argument_list" {
94+
return
95+
}
96+
97+
systemCallArgListNode := getNamedChildren(argsListNode, 0)
98+
99+
for _, argNode := range systemCallArgListNode {
100+
if isTaintedCmdString(argNode, pass.FileContext.Source, eventVarMap, cmdVarMap, intermVarMap) {
101+
pass.Report(pass, node, "Detected `os` system call with tainted `event` data - potential command injection")
102+
} else if isDangerousVar(argNode, pass.FileContext.Source, eventVarMap, intermVarMap) {
103+
pass.Report(pass, node, "Detected `os` system call with tainted `event` data - potential command injection")
104+
}
105+
}
106+
})
107+
108+
return nil, nil
109+
}
110+
111+
func isDangerousVar(node *sitter.Node, source []byte, eventVarMap, intermVarMap map[string]bool) bool {
112+
if node.Type() != "identifier" {
113+
return false
114+
}
115+
116+
return eventVarMap[node.Content(source)] || intermVarMap[node.Content(source)]
117+
}
118+
119+
func isTaintedCmdString(node *sitter.Node, source []byte, eventVarMap, cmdVarMap, intermVarMap map[string]bool) bool {
120+
switch node.Type() {
121+
case "call":
122+
funcNode := node.ChildByFieldName("function")
123+
124+
if funcNode.Type() != "attribute" {
125+
return false
126+
}
127+
128+
funcObjNode := funcNode.ChildByFieldName("object")
129+
funcAttrNode := funcNode.ChildByFieldName("attribute")
130+
131+
if funcAttrNode.Type() != "identifier" {
132+
return false
133+
}
134+
135+
if funcAttrNode.Content(source) != "format" {
136+
return false
137+
}
138+
139+
argsNode := node.ChildByFieldName("arguments")
140+
argsList := getNamedChildren(argsNode, 0)
141+
142+
if funcObjNode.Type() == "identifier" {
143+
if !cmdVarMap[funcObjNode.Content(source)] {
144+
return false
145+
}
146+
147+
for _, argVal := range argsList {
148+
if argVal.Type() == "identifier" {
149+
if eventVarMap[argVal.Content(source)] {
150+
return true
151+
}
152+
} else if argVal.Type() == "subscript" {
153+
return isEventSubscript(argVal, source)
154+
}
155+
}
156+
} else if funcObjNode.Type() == "string" {
157+
for _, argVal := range argsList {
158+
if argVal.Type() == "identifier" {
159+
if eventVarMap[argVal.Content(source)] {
160+
return true
161+
} else if argVal.Type() == "subscript" {
162+
return isEventSubscript(argVal, source)
163+
}
164+
}
165+
}
166+
}
167+
168+
case "binary_operator":
169+
leftNode := node.ChildByFieldName("left")
170+
rightNode := node.ChildByFieldName("right")
171+
172+
if leftNode.Type() == "string" {
173+
if rightNode.Type() == "tuple" {
174+
tupleArgNodes := getNamedChildren(rightNode, 0)
175+
for _, tupArg := range tupleArgNodes {
176+
if tupArg.Type() == "identifier" {
177+
if eventVarMap[tupArg.Content(source)] {
178+
return true
179+
}
180+
} else if tupArg.Type() == "subscript" {
181+
return isEventSubscript(tupArg, source)
182+
}
183+
}
184+
} else if rightNode.Type() == "identifier" {
185+
return eventVarMap[rightNode.Content(source)]
186+
}
187+
} else if leftNode.Type() == "identifier" {
188+
if !cmdVarMap[leftNode.Content(source)] {
189+
return false
190+
}
191+
192+
if rightNode.Type() == "tuple" {
193+
tupleArgNodes := getNamedChildren(rightNode, 0)
194+
195+
for _, tupArg := range tupleArgNodes {
196+
if tupArg.Type() == "identifier" {
197+
if eventVarMap[tupArg.Content(source)] {
198+
return true
199+
} else if tupArg.Type() == "subscript" {
200+
return isEventSubscript(tupArg, source)
201+
}
202+
}
203+
}
204+
}
205+
}
206+
207+
case "string":
208+
if node.Content(source)[0] != 'f' {
209+
return false
210+
}
211+
allStringChildren := getNamedChildren(node, 0)
212+
for _, strnode := range allStringChildren {
213+
if strnode.Type() == "interpolation" {
214+
exprNode := strnode.ChildByFieldName("expression")
215+
if isEventSubscript(exprNode, source) {
216+
return true
217+
}
218+
219+
if eventVarMap[exprNode.Content(source)] {
220+
return true
221+
}
222+
}
223+
}
224+
225+
}
226+
227+
return false
228+
}
229+
230+
func isEventSubscript(node *sitter.Node, source []byte) bool {
231+
if node.Type() != "subscript" {
232+
return false
233+
}
234+
235+
valIdNode := node
236+
237+
// when there are more than 1 subscript accesses, we need to go down the tree
238+
// to get to the identifier
239+
for valIdNode.Type() != "identifier" {
240+
valIdNode = valIdNode.ChildByFieldName("value")
241+
}
242+
243+
eventIdentifier := valIdNode.Content(source)
244+
return strings.Contains(eventIdentifier, "event")
245+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import os
2+
3+
def handler(event, context):
4+
# ok: dangerous-system-call
5+
os.system("ls -al")
6+
7+
# ok: dangerous-system-call
8+
os.popen("cat contents.txt")
9+
10+
# <expect-error>
11+
os.system(f"ls -la {event['dir']}")
12+
13+
eventVar1 = event['cmd']
14+
eventFlag = event['flag']
15+
cmdstr = "ls -la {} {}"
16+
17+
# <expect-error>
18+
os.popen2("sudo rm -rf {}".format(eventVar1))
19+
20+
# <expect-error>
21+
os.system(cmdstr.format(eventFlag, eventVar1))
22+
23+
intermVar = cmdstr % (eventVar1, eventFlag)
24+
25+
# <expect-error>
26+
os.popen(intermVar)

0 commit comments

Comments
 (0)