skylos 2.2.3__py3-none-any.whl → 2.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +1 -1
- skylos/analyzer.py +119 -67
- skylos/cli.py +172 -10
- skylos/rules/danger/__init__.py +0 -0
- skylos/rules/danger/danger.py +141 -0
- skylos/rules/danger/danger_cmd/__init__.py +0 -0
- skylos/rules/danger/danger_cmd/cmd_flow.py +208 -0
- skylos/rules/danger/danger_fs/__init__.py +0 -0
- skylos/rules/danger/danger_fs/path_flow.py +188 -0
- skylos/rules/danger/danger_net/__init__.py +0 -0
- skylos/rules/danger/danger_net/ssrf_flow.py +198 -0
- skylos/rules/danger/danger_sql/__init__.py +0 -0
- skylos/rules/danger/danger_sql/sql_flow.py +175 -0
- skylos/rules/danger/danger_sql/sql_raw_flow.py +202 -0
- skylos/rules/danger/danger_web/__init__.py +0 -0
- skylos/rules/danger/danger_web/xss_flow.py +279 -0
- skylos/rules/secrets.py +34 -5
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/METADATA +1 -1
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/RECORD +28 -10
- test/test_cmd_injection.py +41 -0
- test/test_dangerous.py +101 -0
- test/test_path_traversal.py +40 -0
- test/test_secrets.py +24 -10
- test/test_sql_injection.py +54 -0
- test/test_ssrf.py +51 -0
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/WHEEL +0 -0
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/entry_points.txt +0 -0
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
def _qualified_name_from_call(node):
|
|
6
|
+
func = node.func
|
|
7
|
+
parts = []
|
|
8
|
+
while isinstance(func, ast.Attribute):
|
|
9
|
+
parts.append(func.attr)
|
|
10
|
+
func = func.value
|
|
11
|
+
if isinstance(func, ast.Name):
|
|
12
|
+
parts.append(func.id)
|
|
13
|
+
parts.reverse()
|
|
14
|
+
return ".".join(parts)
|
|
15
|
+
if isinstance(func, ast.Name):
|
|
16
|
+
return func.id
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
def _is_interpolated_string(node):
|
|
20
|
+
if isinstance(node, ast.JoinedStr):
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "format":
|
|
27
|
+
return True
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def _add_finding(findings, file_path, node, rule_id, severity, message):
|
|
31
|
+
findings.append({
|
|
32
|
+
"rule_id": rule_id,
|
|
33
|
+
"severity": severity,
|
|
34
|
+
"message": message,
|
|
35
|
+
"file": str(file_path),
|
|
36
|
+
"line": getattr(node, "lineno", 1),
|
|
37
|
+
"col": getattr(node, "col_offset", 0),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
class _CmdFlowChecker(ast.NodeVisitor):
|
|
41
|
+
OS_SYSTEM = "os.system"
|
|
42
|
+
SUBPROC_PREFIX = "subprocess."
|
|
43
|
+
|
|
44
|
+
def __init__(self, file_path, findings):
|
|
45
|
+
self.file_path = file_path
|
|
46
|
+
self.findings = findings
|
|
47
|
+
self.env_stack = [{}]
|
|
48
|
+
self.current_function = None
|
|
49
|
+
|
|
50
|
+
def _push(self):
|
|
51
|
+
self.env_stack.append({})
|
|
52
|
+
|
|
53
|
+
def _pop(self):
|
|
54
|
+
popped = self.env_stack.pop() # pragma: no skylos
|
|
55
|
+
|
|
56
|
+
def _set(self, name, tainted):
|
|
57
|
+
if not self.env_stack:
|
|
58
|
+
self._push()
|
|
59
|
+
self.env_stack[-1][name] = tainted
|
|
60
|
+
|
|
61
|
+
def _get(self, name):
|
|
62
|
+
for i, env in enumerate(reversed(self.env_stack)):
|
|
63
|
+
if name in env:
|
|
64
|
+
result = env[name]
|
|
65
|
+
return result
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def _tainted(self, node):
|
|
69
|
+
|
|
70
|
+
if _is_interpolated_string(node):
|
|
71
|
+
if isinstance(node, ast.JoinedStr):
|
|
72
|
+
for value in node.values:
|
|
73
|
+
if isinstance(value, ast.FormattedValue):
|
|
74
|
+
if self._tainted(value.value):
|
|
75
|
+
return True
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "input":
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
82
|
+
base = node.value
|
|
83
|
+
while isinstance(base, ast.Attribute):
|
|
84
|
+
base = base.value
|
|
85
|
+
if isinstance(base, ast.Name) and base.id == "request":
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
if isinstance(node, ast.Name):
|
|
89
|
+
result = self._get(node.id)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
93
|
+
target_value = node.value
|
|
94
|
+
result = self._tainted(target_value)
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
if isinstance(node, ast.BinOp):
|
|
98
|
+
left = self._tainted(node.left)
|
|
99
|
+
right = self._tainted(node.right)
|
|
100
|
+
result = left or right
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
if isinstance(node, ast.Call):
|
|
104
|
+
for arg in node.args:
|
|
105
|
+
if self._tainted(arg):
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def _traverse_children(self, node):
|
|
112
|
+
for child in ast.iter_child_nodes(node):
|
|
113
|
+
self.visit(child)
|
|
114
|
+
|
|
115
|
+
def visit_FunctionDef(self, node):
|
|
116
|
+
self.current_function = node.name
|
|
117
|
+
self._push()
|
|
118
|
+
|
|
119
|
+
# for arg in node.args.args:
|
|
120
|
+
# self._set(arg.arg, True)
|
|
121
|
+
|
|
122
|
+
self._traverse_children(node)
|
|
123
|
+
self._pop()
|
|
124
|
+
self.current_function = None
|
|
125
|
+
|
|
126
|
+
def visit_AsyncFunctionDef(self, node):
|
|
127
|
+
self.current_function = node.name
|
|
128
|
+
self._push()
|
|
129
|
+
|
|
130
|
+
for arg in node.args.args:
|
|
131
|
+
self._set(arg.arg, True)
|
|
132
|
+
|
|
133
|
+
self._traverse_children(node)
|
|
134
|
+
self._pop()
|
|
135
|
+
self.current_function = None
|
|
136
|
+
|
|
137
|
+
def visit_Assign(self, node):
|
|
138
|
+
taint = self._tainted(node.value)
|
|
139
|
+
for tgt in node.targets:
|
|
140
|
+
if isinstance(tgt, ast.Name):
|
|
141
|
+
self._set(tgt.id, taint)
|
|
142
|
+
self._traverse_children(node)
|
|
143
|
+
|
|
144
|
+
def visit_AnnAssign(self, node):
|
|
145
|
+
taint = self._tainted(node.value) if node.value else False
|
|
146
|
+
if isinstance(node.target, ast.Name):
|
|
147
|
+
self._set(node.target.id, taint)
|
|
148
|
+
self._traverse_children(node)
|
|
149
|
+
|
|
150
|
+
def visit_AugAssign(self, node):
|
|
151
|
+
taint = self._tainted(node.target) or self._tainted(node.value)
|
|
152
|
+
if isinstance(node.target, ast.Name):
|
|
153
|
+
self._set(node.target.id, taint)
|
|
154
|
+
self._traverse_children(node)
|
|
155
|
+
|
|
156
|
+
def visit_Call(self, node):
|
|
157
|
+
qn = _qualified_name_from_call(node)
|
|
158
|
+
|
|
159
|
+
if qn == self.OS_SYSTEM and node.args:
|
|
160
|
+
arg0 = node.args[0]
|
|
161
|
+
is_interp = _is_interpolated_string(arg0)
|
|
162
|
+
is_taint = self._tainted(arg0)
|
|
163
|
+
|
|
164
|
+
if is_interp or is_taint:
|
|
165
|
+
_add_finding(
|
|
166
|
+
self.findings, self.file_path, node,
|
|
167
|
+
"SKY-D212", "CRITICAL",
|
|
168
|
+
"Possible command injection (RCE): string-built or tainted shell command."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if qn and qn.startswith(self.SUBPROC_PREFIX) and node.args:
|
|
172
|
+
shell_true = False
|
|
173
|
+
for kw in (node.keywords or []):
|
|
174
|
+
if kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
|
|
175
|
+
shell_true = True
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
if shell_true:
|
|
179
|
+
arg0 = node.args[0]
|
|
180
|
+
is_interp = _is_interpolated_string(arg0)
|
|
181
|
+
is_taint = self._tainted(arg0)
|
|
182
|
+
|
|
183
|
+
if is_interp or is_taint:
|
|
184
|
+
_add_finding(
|
|
185
|
+
self.findings, self.file_path, node,
|
|
186
|
+
"SKY-D212", "CRITICAL",
|
|
187
|
+
"Possible command injection (RCE): string-built or tainted command with shell=True."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self._traverse_children(node)
|
|
191
|
+
|
|
192
|
+
def generic_visit(self, node):
|
|
193
|
+
for field, value in ast.iter_fields(node):
|
|
194
|
+
if isinstance(value, list):
|
|
195
|
+
for item in value:
|
|
196
|
+
if isinstance(item, ast.AST):
|
|
197
|
+
self.visit(item)
|
|
198
|
+
elif isinstance(value, ast.AST):
|
|
199
|
+
self.visit(value)
|
|
200
|
+
|
|
201
|
+
def scan(tree, file_path, findings):
|
|
202
|
+
try:
|
|
203
|
+
|
|
204
|
+
checker = _CmdFlowChecker(file_path, findings)
|
|
205
|
+
checker.visit(tree)
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print(f"CMD flow failed for {file_path}: {e}", file=sys.stderr)
|
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
def _qualified_name_from_call(node):
|
|
6
|
+
func = node.func
|
|
7
|
+
parts = []
|
|
8
|
+
while isinstance(func, ast.Attribute):
|
|
9
|
+
parts.append(func.attr)
|
|
10
|
+
func = func.value
|
|
11
|
+
|
|
12
|
+
if isinstance(func, ast.Name):
|
|
13
|
+
parts.append(func.id)
|
|
14
|
+
parts.reverse()
|
|
15
|
+
return ".".join(parts)
|
|
16
|
+
|
|
17
|
+
if isinstance(func, ast.Name):
|
|
18
|
+
return func.id
|
|
19
|
+
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def _is_interpolated_string(node):
|
|
23
|
+
if isinstance(node, ast.JoinedStr):
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "format":
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def _add_finding(findings, file_path, node, rule_id, severity, message):
|
|
35
|
+
findings.append({
|
|
36
|
+
"rule_id": rule_id,
|
|
37
|
+
"severity": severity,
|
|
38
|
+
"message": message,
|
|
39
|
+
"file": str(file_path),
|
|
40
|
+
"line": getattr(node, "lineno", 1),
|
|
41
|
+
"col": getattr(node, "col_offset", 0),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
class _PathFlowChecker(ast.NodeVisitor):
|
|
45
|
+
|
|
46
|
+
FILE_OPEN_FUNCS = {"open"}
|
|
47
|
+
OS_FILE_FUNCS = {"open", "unlink", "remove", "mkdir", "rmdir", "makedirs"}
|
|
48
|
+
SHUTIL_FUNCS = {"copy", "copy2", "copytree", "move", "rmtree"}
|
|
49
|
+
|
|
50
|
+
def __init__(self, file_path, findings):
|
|
51
|
+
self.file_path = file_path
|
|
52
|
+
self.findings = findings
|
|
53
|
+
self.env_stack = [{}]
|
|
54
|
+
|
|
55
|
+
def _push(self):
|
|
56
|
+
self.env_stack.append({})
|
|
57
|
+
|
|
58
|
+
def _pop(self):
|
|
59
|
+
self.env_stack.pop()
|
|
60
|
+
|
|
61
|
+
def _set(self, name, tainted):
|
|
62
|
+
if not self.env_stack:
|
|
63
|
+
self._push()
|
|
64
|
+
self.env_stack[-1][name] = bool(tainted)
|
|
65
|
+
|
|
66
|
+
def _get(self, name):
|
|
67
|
+
for env in reversed(self.env_stack):
|
|
68
|
+
if name in env:
|
|
69
|
+
return env[name]
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def _tainted(self, node):
|
|
73
|
+
if _is_interpolated_string(node):
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "input":
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
80
|
+
base = node.value
|
|
81
|
+
while isinstance(base, ast.Attribute):
|
|
82
|
+
base = base.value
|
|
83
|
+
if isinstance(base, ast.Name) and base.id == "request":
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
if isinstance(node, ast.Name):
|
|
87
|
+
value = self._get(node.id)
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
91
|
+
inner = node.value
|
|
92
|
+
result = self._tainted(inner)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
if isinstance(node, ast.BinOp):
|
|
96
|
+
left = self._tainted(node.left)
|
|
97
|
+
right = self._tainted(node.right)
|
|
98
|
+
if left:
|
|
99
|
+
return True
|
|
100
|
+
if right:
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
if isinstance(node, ast.Call):
|
|
105
|
+
for arg in node.args:
|
|
106
|
+
if self._tainted(arg):
|
|
107
|
+
return True
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def _traverse_children(self, node):
|
|
113
|
+
for child in ast.iter_child_nodes(node):
|
|
114
|
+
self.visit(child)
|
|
115
|
+
|
|
116
|
+
def visit_FunctionDef(self, node):
|
|
117
|
+
self._push()
|
|
118
|
+
for arg in node.args.args:
|
|
119
|
+
self._set(arg.arg, True)
|
|
120
|
+
self._traverse_children(node)
|
|
121
|
+
self._pop()
|
|
122
|
+
|
|
123
|
+
def visit_AsyncFunctionDef(self, node):
|
|
124
|
+
self._push()
|
|
125
|
+
self._traverse_children(node)
|
|
126
|
+
self._pop()
|
|
127
|
+
|
|
128
|
+
def visit_Assign(self, node):
|
|
129
|
+
t = self._tainted(node.value)
|
|
130
|
+
for tgt in node.targets:
|
|
131
|
+
if isinstance(tgt, ast.Name):
|
|
132
|
+
self._set(tgt.id, t)
|
|
133
|
+
self._traverse_children(node)
|
|
134
|
+
|
|
135
|
+
def visit_AnnAssign(self, node):
|
|
136
|
+
if node.value is not None:
|
|
137
|
+
t = self._tainted(node.value)
|
|
138
|
+
else:
|
|
139
|
+
t = False
|
|
140
|
+
if isinstance(node.target, ast.Name):
|
|
141
|
+
self._set(node.target.id, t)
|
|
142
|
+
self._traverse_children(node)
|
|
143
|
+
|
|
144
|
+
def visit_AugAssign(self, node):
|
|
145
|
+
t = self._tainted(node.target) or self._tainted(node.value)
|
|
146
|
+
if isinstance(node.target, ast.Name):
|
|
147
|
+
self._set(node.target.id, t)
|
|
148
|
+
self._traverse_children(node)
|
|
149
|
+
|
|
150
|
+
def _flag_if_tainted_path(self, node, path_expr):
|
|
151
|
+
is_interp = _is_interpolated_string(path_expr)
|
|
152
|
+
is_tainted = self._tainted(path_expr)
|
|
153
|
+
if is_interp or is_tainted:
|
|
154
|
+
_add_finding(
|
|
155
|
+
self.findings, self.file_path, node,
|
|
156
|
+
"SKY-D215", "HIGH",
|
|
157
|
+
"Possible path traversal: tainted filesystem path"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def visit_Call(self, node):
|
|
161
|
+
qn = _qualified_name_from_call(node)
|
|
162
|
+
|
|
163
|
+
if qn and qn in self.FILE_OPEN_FUNCS and node.args:
|
|
164
|
+
self._flag_if_tainted_path(node, node.args[0])
|
|
165
|
+
|
|
166
|
+
if qn and "." in qn:
|
|
167
|
+
mod, func = qn.split(".", 1)
|
|
168
|
+
if mod == "os" and func in self.OS_FILE_FUNCS and node.args:
|
|
169
|
+
self._flag_if_tainted_path(node, node.args[0])
|
|
170
|
+
|
|
171
|
+
if mod == "shutil" and func in self.SHUTIL_FUNCS and node.args:
|
|
172
|
+
self._flag_if_tainted_path(node, node.args[0])
|
|
173
|
+
|
|
174
|
+
if func == "open" and node.args:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
self._traverse_children(node)
|
|
178
|
+
|
|
179
|
+
def generic_visit(self, node):
|
|
180
|
+
for child in ast.iter_child_nodes(node):
|
|
181
|
+
self.visit(child)
|
|
182
|
+
|
|
183
|
+
def scan(tree, file_path, findings):
|
|
184
|
+
try:
|
|
185
|
+
checker = _PathFlowChecker(file_path, findings)
|
|
186
|
+
checker.visit(tree)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f"Path traversal analysis failed for {file_path}: {e}", file=sys.stderr)
|
|
File without changes
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
url = input()
|
|
7
|
+
requests.get(url) # attacker controls destination.. can hit internal metadata services, etc.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def _qualified_name_from_call(node):
|
|
11
|
+
func = node.func
|
|
12
|
+
parts = []
|
|
13
|
+
while isinstance(func, ast.Attribute):
|
|
14
|
+
parts.append(func.attr)
|
|
15
|
+
func = func.value
|
|
16
|
+
|
|
17
|
+
if isinstance(func, ast.Name):
|
|
18
|
+
parts.append(func.id)
|
|
19
|
+
parts.reverse()
|
|
20
|
+
return ".".join(parts)
|
|
21
|
+
|
|
22
|
+
if isinstance(func, ast.Name):
|
|
23
|
+
return func.id
|
|
24
|
+
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def _is_interpolated_string(node):
|
|
28
|
+
if isinstance(node, ast.JoinedStr):
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "format":
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def _add_finding(findings, file_path, node, rule_id, severity, message):
|
|
40
|
+
findings.append({
|
|
41
|
+
"rule_id": rule_id,
|
|
42
|
+
"severity": severity,
|
|
43
|
+
"message": message,
|
|
44
|
+
"file": str(file_path),
|
|
45
|
+
"line": getattr(node, "lineno", 1),
|
|
46
|
+
"col": getattr(node, "col_offset", 0),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
class _SSRFFlowChecker(ast.NodeVisitor):
|
|
50
|
+
|
|
51
|
+
HTTP_METHODS = {"get", "post", "put", "delete", "head", "options", "request"}
|
|
52
|
+
|
|
53
|
+
def __init__(self, file_path, findings):
|
|
54
|
+
self.file_path = file_path
|
|
55
|
+
self.findings = findings
|
|
56
|
+
self.env_stack = []
|
|
57
|
+
|
|
58
|
+
def _push(self):
|
|
59
|
+
self.env_stack.append({})
|
|
60
|
+
|
|
61
|
+
def _pop(self):
|
|
62
|
+
self.env_stack.pop()
|
|
63
|
+
|
|
64
|
+
def _set(self, name, tainted):
|
|
65
|
+
if not self.env_stack:
|
|
66
|
+
self._push()
|
|
67
|
+
self.env_stack[-1][name] = bool(tainted)
|
|
68
|
+
|
|
69
|
+
def _get(self, name):
|
|
70
|
+
for env in reversed(self.env_stack):
|
|
71
|
+
if name in env:
|
|
72
|
+
return env[name]
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def _tainted(self, node):
|
|
76
|
+
if _is_interpolated_string(node):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "input":
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
if isinstance(node, ast.Call):
|
|
83
|
+
qn = _qualified_name_from_call(node)
|
|
84
|
+
if qn and qn.startswith("request."):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
88
|
+
base = node.value
|
|
89
|
+
while isinstance(base, ast.Attribute):
|
|
90
|
+
base = base.value
|
|
91
|
+
if isinstance(base, ast.Name) and base.id == "request":
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
if isinstance(node, ast.Name):
|
|
95
|
+
value = self._get(node.id)
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
99
|
+
inner = node.value
|
|
100
|
+
result = self._tainted(inner)
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
if isinstance(node, ast.BinOp):
|
|
104
|
+
left = self._tainted(node.left)
|
|
105
|
+
right = self._tainted(node.right)
|
|
106
|
+
if left:
|
|
107
|
+
return True
|
|
108
|
+
if right:
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
if isinstance(node, ast.Call):
|
|
113
|
+
for arg in node.args:
|
|
114
|
+
if self._tainted(arg):
|
|
115
|
+
return True
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
def visit_FunctionDef(self, node):
|
|
121
|
+
self._push()
|
|
122
|
+
self.generic_visit(node)
|
|
123
|
+
self._pop()
|
|
124
|
+
|
|
125
|
+
def visit_AsyncFunctionDef(self, node):
|
|
126
|
+
self._push()
|
|
127
|
+
self.generic_visit(node)
|
|
128
|
+
self._pop()
|
|
129
|
+
|
|
130
|
+
def visit_Assign(self, node):
|
|
131
|
+
taint = self._tainted(node.value)
|
|
132
|
+
for tgt in node.targets:
|
|
133
|
+
if isinstance(tgt, ast.Name):
|
|
134
|
+
self._set(tgt.id, taint)
|
|
135
|
+
self.generic_visit(node)
|
|
136
|
+
|
|
137
|
+
def visit_AnnAssign(self, node):
|
|
138
|
+
if node.value is not None:
|
|
139
|
+
taint = self._tainted(node.value)
|
|
140
|
+
else:
|
|
141
|
+
taint = False
|
|
142
|
+
|
|
143
|
+
if isinstance(node.target, ast.Name):
|
|
144
|
+
self._set(node.target.id, taint)
|
|
145
|
+
self.generic_visit(node)
|
|
146
|
+
|
|
147
|
+
def visit_AugAssign(self, node):
|
|
148
|
+
taint = self._tainted(node.target) or self._tainted(node.value)
|
|
149
|
+
if isinstance(node.target, ast.Name):
|
|
150
|
+
self._set(node.target.id, taint)
|
|
151
|
+
self.generic_visit(node)
|
|
152
|
+
|
|
153
|
+
def visit_Call(self, node):
|
|
154
|
+
qn = _qualified_name_from_call(node)
|
|
155
|
+
|
|
156
|
+
if qn and "." in qn:
|
|
157
|
+
_, func = qn.split(".", 1)
|
|
158
|
+
if func in self.HTTP_METHODS and node.args:
|
|
159
|
+
url_arg = node.args[0]
|
|
160
|
+
is_interp = _is_interpolated_string(url_arg)
|
|
161
|
+
is_tainted = self._tainted(url_arg)
|
|
162
|
+
|
|
163
|
+
if is_interp or is_tainted:
|
|
164
|
+
_add_finding(
|
|
165
|
+
self.findings, self.file_path, node,
|
|
166
|
+
"SKY-D216", "CRITICAL",
|
|
167
|
+
"Possible SSRF: tainted URL passed to HTTP client."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if qn and qn.endswith(".urlopen") and node.args:
|
|
171
|
+
url_arg = node.args[0]
|
|
172
|
+
is_interp = _is_interpolated_string(url_arg)
|
|
173
|
+
is_tainted = self._tainted(url_arg)
|
|
174
|
+
|
|
175
|
+
if is_interp or is_tainted:
|
|
176
|
+
_add_finding(
|
|
177
|
+
self.findings, self.file_path, node,
|
|
178
|
+
"SKY-D216", "CRITICAL",
|
|
179
|
+
"Possible SSRF: tainted URL passed to HTTP client."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
self.generic_visit(node)
|
|
183
|
+
|
|
184
|
+
def generic_visit(self, node):
|
|
185
|
+
for field, value in ast.iter_fields(node):
|
|
186
|
+
if isinstance(value, list):
|
|
187
|
+
for item in value:
|
|
188
|
+
if isinstance(item, ast.AST):
|
|
189
|
+
self.visit(item)
|
|
190
|
+
elif isinstance(value, ast.AST):
|
|
191
|
+
self.visit(value)
|
|
192
|
+
|
|
193
|
+
def scan(tree, file_path, findings):
|
|
194
|
+
try:
|
|
195
|
+
checker = _SSRFFlowChecker(file_path, findings)
|
|
196
|
+
checker.visit(tree)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f"SSRF flow analysis failed for {file_path}: {e}", file=sys.stderr)
|
|
File without changes
|