Skip to content

Commit e6aee7c

Browse files
committed
checkers(python): add checkers to detect event tainted SQL strings in AWS lambda handler functions
Signed-off-by: Maharshi Basu <basumaharshi10@gmail.com>
1 parent 2b1cc3d commit e6aee7c

File tree

3 files changed

+254
-1
lines changed

3 files changed

+254
-1
lines changed

checkers/checker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var AnalyzerRegistry = []Analyzer{
7272
},
7373
{
7474
TestDir: "checkers/python/testdata",
75-
Analyzers: []*goAnalysis.Analyzer{python.InsecureUrllibFtp},
75+
Analyzers: []*goAnalysis.Analyzer{python.InsecureUrllibFtp, python.AwsLambdaTaintedSqlString},
7676
},
7777
}
7878

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package python
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
sitter "github.com/smacker/go-tree-sitter"
8+
"globstar.dev/analysis"
9+
)
10+
11+
var AwsLambdaTaintedSqlString *analysis.Analyzer = &analysis.Analyzer{
12+
Name: "aws-lambda-tainted-sql-string",
13+
Language: analysis.LangPy,
14+
Description: "Detected user input directly used to build a SQL string, which can lead to SQL injection vulnerabilities. This could allow an attacker to steal or modify database contents. Use parameterized queries or an ORM like Sequelize to protect against injection attacks.",
15+
Category: analysis.CategorySecurity,
16+
Severity: analysis.SeverityWarning,
17+
Run: checkAwsLambdaTaintedSqlString,
18+
}
19+
20+
func checkAwsLambdaTaintedSqlString(pass *analysis.Pass) (interface{}, error) {
21+
eventVarMap := make(map[string]bool)
22+
sqlStringVarMap := make(map[string]bool)
23+
24+
// store variables for event subscripts and sql strings
25+
analysis.Preorder(pass, func(node *sitter.Node) {
26+
if node.Type() != "assignment" {
27+
return
28+
}
29+
30+
leftNode := node.ChildByFieldName("left")
31+
rightNode := node.ChildByFieldName("right")
32+
33+
if rightNode == nil {
34+
return
35+
}
36+
37+
if isEventSubscript(rightNode, pass.FileContext.Source) {
38+
eventVarMap[leftNode.Content(pass.FileContext.Source)] = true
39+
}
40+
41+
if isSqlString(rightNode, pass.FileContext.Source) {
42+
sqlStringVarMap[leftNode.Content(pass.FileContext.Source)] = true
43+
}
44+
})
45+
46+
// detect tainted sql strings in assignments
47+
analysis.Preorder(pass, func(node *sitter.Node) {
48+
if node.Type() != "assignment" {
49+
return
50+
}
51+
52+
rightNode := node.ChildByFieldName("right")
53+
54+
if rightNode == nil {
55+
return
56+
}
57+
58+
if isTaintedSqlString(rightNode, pass.FileContext.Source, sqlStringVarMap, eventVarMap) {
59+
pass.Report(pass, node, "Detect `event` tainted SQL string - Potential SQL injection vulnerabilities")
60+
}
61+
})
62+
63+
return nil, nil
64+
}
65+
66+
func isTaintedSqlString(node *sitter.Node, source []byte, sqlStringVarMap, eventVarMap map[string]bool) bool {
67+
switch node.Type() {
68+
case "binary_operator":
69+
leftNode := node.ChildByFieldName("left")
70+
rightNode := node.ChildByFieldName("right")
71+
72+
if leftNode.Type() == "string" {
73+
if rightNode.Type() == "tuple" {
74+
tupleArgNodes := getNamedChildren(rightNode, 0)
75+
for _, tupArg := range tupleArgNodes {
76+
if tupArg.Type() == "identifier" {
77+
if eventVarMap[tupArg.Content(source)] {
78+
return true
79+
}
80+
} else if tupArg.Type() == "subscript" {
81+
return isEventSubscript(tupArg, source)
82+
}
83+
}
84+
85+
} else if rightNode.Type() == "identifier" {
86+
return eventVarMap[rightNode.Content(source)]
87+
}
88+
89+
} else if leftNode.Type() == "identifier" {
90+
if !sqlStringVarMap[leftNode.Content(source)] {
91+
return false
92+
}
93+
94+
if rightNode.Type() == "tuple" {
95+
tupleArgNodes := getNamedChildren(rightNode, 0)
96+
97+
for _, tupArg := range tupleArgNodes {
98+
if tupArg.Type() == "identifier" {
99+
if eventVarMap[tupArg.Content(source)] {
100+
return true
101+
}
102+
} else if tupArg.Type() == "subscript" {
103+
return isEventSubscript(tupArg, source)
104+
}
105+
}
106+
}
107+
}
108+
109+
case "call":
110+
funcNode := node.ChildByFieldName("function")
111+
if funcNode.Type() != "attribute" {
112+
return false
113+
}
114+
115+
funcObjNode := funcNode.ChildByFieldName("object")
116+
funcAttrNode := funcNode.ChildByFieldName("attribute")
117+
118+
if funcAttrNode.Type() != "identifier" {
119+
return false
120+
}
121+
122+
if funcAttrNode.Content(source) != "format" {
123+
return false
124+
}
125+
126+
argsNode := node.ChildByFieldName("arguments")
127+
argsList := getNamedChildren(argsNode, 0)
128+
129+
if funcObjNode.Type() == "string" {
130+
for _, argVal := range argsList {
131+
if argVal.Type() == "identifier" {
132+
if eventVarMap[argVal.Content(source)] {
133+
return true
134+
}
135+
} else if argVal.Type() == "subscript" {
136+
return isEventSubscript(argVal, source)
137+
}
138+
}
139+
} else if funcObjNode.Type() == "identifier" {
140+
if !sqlStringVarMap[funcObjNode.Content(source)] {
141+
return false
142+
}
143+
144+
for _, argVal := range argsList {
145+
if argVal.Type() == "identifier" {
146+
if eventVarMap[argVal.Content(source)] {
147+
return true
148+
}
149+
} else if argVal.Type() == "subscript" {
150+
return isEventSubscript(argVal, source)
151+
}
152+
}
153+
}
154+
155+
case "string":
156+
if node.Content(source)[0] != 'f' {
157+
return false
158+
}
159+
160+
allStringChildren := getNamedChildren(node, 0)
161+
for _, strnode := range allStringChildren {
162+
if strnode.Type() == "interpolation" {
163+
exprNode := strnode.ChildByFieldName("expression")
164+
if isEventSubscript(exprNode, source) {
165+
return true
166+
}
167+
168+
if eventVarMap[exprNode.Content(source)] {
169+
return true
170+
}
171+
}
172+
}
173+
}
174+
175+
return false
176+
}
177+
178+
func isSqlString(node *sitter.Node, source []byte) bool {
179+
if node.Type() != "string" {
180+
return false
181+
}
182+
183+
sqlPattern := regexp.MustCompile(`\s*(?i)(select|delete|insert|create|update|alter|drop)\b.*=`)
184+
return sqlPattern.MatchString(node.Content(source))
185+
}
186+
187+
func isEventSubscript(node *sitter.Node, source []byte) bool {
188+
if node.Type() != "subscript" {
189+
return false
190+
}
191+
192+
valIdNode := node
193+
194+
// when there are more than 1 subscript accesses, we need to go down the tree
195+
// to get to the identifier
196+
for valIdNode.Type() != "identifier" {
197+
valIdNode = valIdNode.ChildByFieldName("value")
198+
}
199+
200+
eventIdentifier := valIdNode.Content(source)
201+
return strings.Contains(eventIdentifier, "event")
202+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import json
2+
import secret_info
3+
import mysql.connector
4+
5+
RemoteMysql = secret_info.RemoteMysql
6+
7+
mydb = mysql.connector.connect(host=RemoteMysql.host, user=RemoteMysql.user, passwd=RemoteMysql.passwd, database=RemoteMysql.database)
8+
mydbCursor = mydb.cursor()
9+
10+
def lambda_handler(event, context):
11+
publicIP=event["queryStringParameters"]["publicIP"]
12+
# <expect-error>
13+
sql = """UPDATE `EC2ServerPublicIP` SET %s = '%s' WHERE %s = %d""" % ("publicIP",publicIP,"ID", 1)
14+
15+
# <expect-error>
16+
sql = "UPDATE %s" % publicIP
17+
18+
sqlstr = """UPDATE `EC2ServerPublicIP` SET %s = '%s' WHERE %s = %d"""
19+
# <expect-error>
20+
sql = sqlstr % ("publicIP",publicIP,"ID", 1)
21+
22+
# <expect-error>
23+
sql = """UPDATE `EC2ServerPublicIP` SET %s = '%s' WHERE %s = %d""".format("publicIP",publicIP,"ID", 1)
24+
25+
# <expect-error>
26+
sql = f"""UPDATE `EC2ServerPublicIP` SET "publicIp" = '{publicIP}' WHERE {event['url']} = {1}"""
27+
28+
# ok: tainted-sql-string
29+
print("""UPDATE `EC2ServerPublicIP` SET %s = '%s' WHERE %s = %d""" % ("publicIP",publicIP,"ID", 1))
30+
31+
mydbCursor.execute(sql)
32+
33+
# ok: tainted-sql-string
34+
sql2 = "UPDATE `EC2ServerPublicIP` SET %s = '%s' WHERE foo = %s" % ("one", "two", "three")
35+
36+
# ok: tainted-sql-string
37+
mydbCursor.execute("UPDATE `EC2ServerPublicIP` SET %s = '%s' WHERE %s = %s", ("publicIP",publicIP,"ID", 1))
38+
39+
# ok: tainted-sql-string
40+
sql3 = "UPDATE `EC2ServerPublicIP` SET %s = '%s'" + " WHERE %s = %s"
41+
mydbCursor.execute(sql3, ("publicIP",publicIP,"ID", 1))
42+
mydb.commit()
43+
44+
Body={
45+
"publicIP":publicIP
46+
47+
}
48+
return {
49+
'statusCode': 200,
50+
'body': json.dumps(Body)
51+
}

0 commit comments

Comments
 (0)