Skip to content

Commit 4abde69

Browse files
committed
checker: request tainted file write + HttpResponse/HttpResponseBadRequest
Signed-off-by: Maharshi Basu <basumaharshi10@gmail.com>
1 parent 2b1cc3d commit 4abde69

File tree

5 files changed

+525
-1
lines changed

5 files changed

+525
-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.DjangoRequestDataWrite, python.DjangoRequestHttpResponse},
7676
},
7777
}
7878

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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 DjangoRequestDataWrite *analysis.Analyzer = &analysis.Analyzer{
12+
Name: "django-request-data-write",
13+
Language: analysis.LangPy,
14+
Description: "User-controlled request data is directly written to a file, which can lead to security risks such as unauthorized file modification, forced log rotation, or denial-of-service by exhausting disk space. Ensure proper input sanitization or escaping to mitigate these threats.",
15+
Category: analysis.CategorySecurity,
16+
Severity: analysis.SeverityWarning,
17+
Run: checkDjangoRequestDataWrite,
18+
}
19+
20+
func checkDjangoRequestDataWrite(pass *analysis.Pass) (interface{}, error) {
21+
reqVarMap := make(map[string]bool)
22+
intermVarMap := make(map[string]bool)
23+
24+
// get var names for data received from `request` calls
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 isRequestCall(rightNode, pass.FileContext.Source) {
38+
reqVarMap[leftNode.Content(pass.FileContext.Source)] = true
39+
}
40+
})
41+
42+
// get var names for data formatted by user data
43+
analysis.Preorder(pass, func(node *sitter.Node) {
44+
if node.Type() != "assignment" {
45+
return
46+
}
47+
48+
leftNode := node.ChildByFieldName("left")
49+
rightNode := node.ChildByFieldName("right")
50+
51+
if rightNode == nil {
52+
return
53+
}
54+
55+
if isUserTainted(rightNode, pass.FileContext.Source, intermVarMap, reqVarMap) {
56+
intermVarMap[leftNode.Content(pass.FileContext.Source)] = true
57+
}
58+
})
59+
60+
// catch insecure file write methods
61+
analysis.Preorder(pass, func(node *sitter.Node) {
62+
if node.Type() != "call" {
63+
return
64+
}
65+
66+
funcNode := node.ChildByFieldName("function")
67+
if funcNode.Type() != "attribute" && funcNode.Type() != "identifier" {
68+
return
69+
}
70+
71+
if !strings.HasSuffix(funcNode.Content(pass.FileContext.Source), "write") {
72+
return
73+
}
74+
75+
argListNode := node.ChildByFieldName("arguments")
76+
if argListNode.Type() != "argument_list" {
77+
return
78+
}
79+
80+
argNodes := getNamedChildren(argListNode, 0)
81+
for _, arg := range argNodes {
82+
if isUserTainted(arg, pass.FileContext.Source, intermVarMap, reqVarMap) {
83+
pass.Report(pass, node, "User-controlled data written to a file may enable log tampering, forced rotation, or disk exhaustion—sanitize input before writing")
84+
}
85+
}
86+
87+
})
88+
89+
return nil, nil
90+
}
91+
92+
func isUserTainted(node *sitter.Node, source []byte, intermVarMap, reqVarMap map[string]bool) bool {
93+
switch node.Type() {
94+
case "call":
95+
if isInFunc(node, source, intermVarMap, reqVarMap) {
96+
return true
97+
}
98+
functionNode := node.ChildByFieldName("function")
99+
if functionNode.Type() != "attribute" {
100+
return false
101+
}
102+
103+
if !strings.HasSuffix(functionNode.Content(source), ".format") {
104+
return false
105+
}
106+
107+
argListNode := node.ChildByFieldName("arguments")
108+
if argListNode.Type() != "argument_list" {
109+
return false
110+
}
111+
112+
argsNode := getNamedChildren(argListNode, 0)
113+
for _, arg := range argsNode {
114+
if arg.Type() == "identifier" && reqVarMap[arg.Content(source)] {
115+
return true
116+
} else if arg.Type() == "call" && isRequestCall(arg, source) {
117+
return true
118+
}
119+
}
120+
121+
case "string":
122+
if node.Content(source)[0] != 'f' {
123+
return false
124+
}
125+
stringChildrenNodes := getNamedChildren(node, 0)
126+
for _, strnode := range stringChildrenNodes {
127+
if strnode.Type() == "interpolation" {
128+
exprnode := strnode.ChildByFieldName("expression")
129+
if exprnode.Type() == "identifier" && reqVarMap[exprnode.Content(source)] {
130+
return true
131+
} else if exprnode.Type() == "call" && isRequestCall(exprnode, source) {
132+
return true
133+
}
134+
}
135+
}
136+
137+
case "binary_operator":
138+
binOpStr := node.Content(source)
139+
140+
for reqvar := range reqVarMap {
141+
pattern := `\b` + reqvar + `\b`
142+
re := regexp.MustCompile(pattern)
143+
144+
if re.MatchString(binOpStr) {
145+
return true
146+
}
147+
}
148+
149+
rightNode := node.ChildByFieldName("right")
150+
if rightNode.Type() == "call" && isRequestCall(rightNode, source) {
151+
return true
152+
} else if rightNode.Type() == "tuple" {
153+
targsNode := getNamedChildren(rightNode, 0)
154+
for _, targ := range targsNode {
155+
if targ.Type() == "identifier" && reqVarMap[targ.Content(source)] {
156+
return true
157+
} else if targ.Type() == "call" && isRequestCall(targ, source) {
158+
return true
159+
}
160+
}
161+
}
162+
163+
case "identifier":
164+
return reqVarMap[node.Content(source)] || intermVarMap[node.Content(source)]
165+
166+
case "subscript":
167+
return isRequestCall(node, source)
168+
}
169+
170+
return false
171+
}
172+
173+
func isInFunc(node *sitter.Node, source []byte, intermVarMap, reqvarmap map[string]bool) bool {
174+
if node.Type() != "call" {
175+
return false
176+
}
177+
if strings.HasSuffix(node.Content(source), ".format") {
178+
return false
179+
}
180+
argListNode := node.ChildByFieldName("arguments")
181+
if argListNode.Type() != "argument_list" {
182+
return false
183+
}
184+
argNodes := getNamedChildren(argListNode, 0)
185+
186+
for _, arg := range argNodes {
187+
if isUserTainted(arg, source, intermVarMap, reqvarmap) {
188+
return true
189+
}
190+
}
191+
return false
192+
}
193+
194+
func isRequestCall(node *sitter.Node, source []byte) bool {
195+
switch node.Type() {
196+
case "call":
197+
funcNode := node.ChildByFieldName("function")
198+
if funcNode.Type() != "attribute" {
199+
return false
200+
}
201+
objectNode := funcNode.ChildByFieldName("object")
202+
if !strings.Contains(objectNode.Content(source), "request") {
203+
return false
204+
}
205+
206+
attributeNode := funcNode.ChildByFieldName("attribute")
207+
if attributeNode.Type() != "identifier" {
208+
return false
209+
}
210+
211+
if !strings.Contains(attributeNode.Content(source), "get") {
212+
return false
213+
}
214+
215+
return true
216+
217+
case "subscript":
218+
valueNode := node.ChildByFieldName("value")
219+
if valueNode.Type() != "attribute" {
220+
return false
221+
}
222+
223+
objNode := valueNode.ChildByFieldName("object")
224+
if objNode.Type() != "identifier" && objNode.Content(source) != "request" {
225+
return false
226+
}
227+
228+
return true
229+
}
230+
231+
return false
232+
}

0 commit comments

Comments
 (0)