Skip to content

Commit 98a7b6b

Browse files
authored
↪️ Merge pull request #198 from wchresta/177_support_from_import_sinks
177 support from import sinks
2 parents 022476a + a7bb0b2 commit 98a7b6b

File tree

8 files changed

+168
-84
lines changed

8 files changed

+168
-84
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os
2+
import os as myos
3+
from os import system
4+
from os import system as mysystem
5+
from subprocess import call as mycall, Popen as mypopen
6+
7+
from flask import Flask, render_template, request
8+
9+
app = Flask(__name__)
10+
11+
12+
@app.route('/menu', methods=['POST'])
13+
def menu():
14+
param = request.form['suggestion']
15+
command = 'echo ' + param + ' >> ' + 'menu.txt'
16+
17+
os.system(command)
18+
myos.system(command)
19+
system(command)
20+
mysystem(command)
21+
mycall(command)
22+
mypopen(command)
23+
24+
with open('menu.txt', 'r') as f:
25+
menu_ctx = f.read()
26+
27+
return render_template('command_injection.html', menu=menu_ctx)
28+
29+
30+
if __name__ == '__main__':
31+
app.run(debug=True)

pyt/cfg/alias_helper.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,21 @@ def retrieve_import_alias_mapping(names_list):
7474
if alias.asname:
7575
import_alias_names[alias.asname] = alias.name
7676
return import_alias_names
77+
78+
79+
def fully_qualify_alias_labels(label, aliases):
80+
"""Replace any aliases in label with the fully qualified name.
81+
82+
Args:
83+
label -- A label : str representing a name (e.g. myos.system)
84+
aliases -- A dict of {alias: real_name} (e.g. {'myos': 'os'})
85+
86+
>>> fully_qualify_alias_labels('myos.mycall', {'myos':'os'})
87+
'os.mycall'
88+
"""
89+
for alias, full_name in aliases.items():
90+
if label == alias:
91+
return full_name
92+
elif label.startswith(alias+'.'):
93+
return full_name + label[len(alias):]
94+
return label

pyt/cfg/stmt_visitor.py

Lines changed: 79 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from .alias_helper import (
88
as_alias_handler,
9+
fully_qualify_alias_labels,
910
handle_aliases_in_init_files,
1011
handle_fdid_aliases,
1112
not_as_alias_handler,
@@ -624,6 +625,11 @@ def add_blackbox_or_builtin_call(self, node, blackbox): # noqa: C901
624625

625626
call_function_label = call_label_visitor.result[:call_label_visitor.result.find('(')]
626627

628+
# Check if function call matches a blackbox/built-in alias and if so, resolve it
629+
# This resolves aliases like "from os import system as mysys" as: mysys -> os.system
630+
local_definitions = self.module_definitions_stack[-1]
631+
call_function_label = fully_qualify_alias_labels(call_function_label, local_definitions.import_alias_mapping)
632+
627633
# Create e.g. ~call_1 = ret_func_foo
628634
LHS = CALL_IDENTIFIER + 'call_' + str(saved_function_call_index)
629635
RHS = 'ret_' + call_function_label + '('
@@ -810,7 +816,7 @@ def add_module( # noqa: C901
810816
module_path = module[1]
811817

812818
parent_definitions = self.module_definitions_stack[-1]
813-
# The only place the import_alias_mapping is updated
819+
# Here, in `visit_Import` and in `visit_ImportFrom` are the only places the `import_alias_mapping` is updated
814820
parent_definitions.import_alias_mapping.update(import_alias_mapping)
815821
parent_definitions.import_names = local_names
816822

@@ -919,10 +925,10 @@ def from_directory_import(
919925
if init_exists and not skip_init:
920926
package_name = os.path.split(module_path)[1]
921927
return self.add_module(
922-
(module[0], init_file_location),
923-
package_name,
924-
local_names,
925-
import_alias_mapping,
928+
module=(module[0], init_file_location),
929+
module_or_package_name=package_name,
930+
local_names=local_names,
931+
import_alias_mapping=import_alias_mapping,
926932
is_init=True,
927933
from_from=True
928934
)
@@ -932,10 +938,10 @@ def from_directory_import(
932938
new_init_file_location = os.path.join(full_name, '__init__.py')
933939
if os.path.isfile(new_init_file_location):
934940
self.add_module(
935-
(real_name, new_init_file_location),
936-
real_name,
937-
local_names,
938-
import_alias_mapping,
941+
module=(real_name, new_init_file_location),
942+
module_or_package_name=real_name,
943+
local_names=local_names,
944+
import_alias_mapping=import_alias_mapping,
939945
is_init=True,
940946
from_from=True,
941947
from_fdid=True
@@ -945,10 +951,10 @@ def from_directory_import(
945951
else:
946952
file_module = (real_name, full_name + '.py')
947953
self.add_module(
948-
file_module,
949-
real_name,
950-
local_names,
951-
import_alias_mapping,
954+
module=file_module,
955+
module_or_package_name=real_name,
956+
local_names=local_names,
957+
import_alias_mapping=import_alias_mapping,
952958
from_from=True
953959
)
954960
return IgnoredNode()
@@ -959,10 +965,10 @@ def import_package(self, module, module_name, local_name, import_alias_mapping):
959965
init_exists = os.path.isfile(init_file_location)
960966
if init_exists:
961967
return self.add_module(
962-
(module[0], init_file_location),
963-
module_name,
964-
local_name,
965-
import_alias_mapping,
968+
module=(module[0], init_file_location),
969+
module_or_package_name=module_name,
970+
local_names=local_name,
971+
import_alias_mapping=import_alias_mapping,
966972
is_init=True
967973
)
968974
else:
@@ -1005,10 +1011,10 @@ def handle_relative_import(self, node):
10051011
# Is it a file?
10061012
if name_with_dir.endswith('.py'):
10071013
return self.add_module(
1008-
(node.module, name_with_dir),
1009-
None,
1010-
as_alias_handler(node.names),
1011-
retrieve_import_alias_mapping(node.names),
1014+
module=(node.module, name_with_dir),
1015+
module_or_package_name=None,
1016+
local_names=as_alias_handler(node.names),
1017+
import_alias_mapping=retrieve_import_alias_mapping(node.names),
10121018
from_from=True
10131019
)
10141020
return self.from_directory_import(
@@ -1031,10 +1037,10 @@ def visit_Import(self, node):
10311037
retrieve_import_alias_mapping(node.names)
10321038
)
10331039
return self.add_module(
1034-
module,
1035-
name.name,
1036-
name.asname,
1037-
retrieve_import_alias_mapping(node.names)
1040+
module=module,
1041+
module_or_package_name=name.name,
1042+
local_names=name.asname,
1043+
import_alias_mapping=retrieve_import_alias_mapping(node.names)
10381044
)
10391045
for module in self.project_modules:
10401046
if name.name == module[0]:
@@ -1046,55 +1052,69 @@ def visit_Import(self, node):
10461052
retrieve_import_alias_mapping(node.names)
10471053
)
10481054
return self.add_module(
1049-
module,
1050-
name.name,
1051-
name.asname,
1052-
retrieve_import_alias_mapping(node.names)
1055+
module=module,
1056+
module_or_package_name=name.name,
1057+
local_names=name.asname,
1058+
import_alias_mapping=retrieve_import_alias_mapping(node.names)
10531059
)
10541060
for alias in node.names:
1061+
# The module is uninspectable (so blackbox or built-in). If it has an alias, we remember
1062+
# the alias so we can do fully qualified name resolution for blackbox- and built-in trigger words
1063+
# e.g. we want a call to "os.system" be recognised, even if we do "import os as myos"
1064+
if alias.asname is not None and alias.asname != alias.name:
1065+
local_definitions = self.module_definitions_stack[-1]
1066+
local_definitions.import_alias_mapping[name.asname] = alias.name
10551067
if alias.name not in uninspectable_modules:
1056-
log.warn("Cannot inspect module %s", alias.name)
1068+
log.warning("Cannot inspect module %s", alias.name)
10571069
uninspectable_modules.add(alias.name) # Don't repeatedly warn about this
10581070
return IgnoredNode()
10591071

10601072
def visit_ImportFrom(self, node):
10611073
# Is it relative?
10621074
if node.level > 0:
10631075
return self.handle_relative_import(node)
1064-
else:
1065-
for module in self.local_modules:
1066-
if node.module == module[0]:
1067-
if os.path.isdir(module[1]):
1068-
return self.from_directory_import(
1069-
module,
1070-
not_as_alias_handler(node.names),
1071-
as_alias_handler(node.names)
1072-
)
1073-
return self.add_module(
1076+
# not relative
1077+
for module in self.local_modules:
1078+
if node.module == module[0]:
1079+
if os.path.isdir(module[1]):
1080+
return self.from_directory_import(
10741081
module,
1075-
None,
1076-
as_alias_handler(node.names),
1077-
retrieve_import_alias_mapping(node.names),
1078-
from_from=True
1082+
not_as_alias_handler(node.names),
1083+
as_alias_handler(node.names)
10791084
)
1080-
for module in self.project_modules:
1081-
name = module[0]
1082-
if node.module == name:
1083-
if os.path.isdir(module[1]):
1084-
return self.from_directory_import(
1085-
module,
1086-
not_as_alias_handler(node.names),
1087-
as_alias_handler(node.names),
1088-
retrieve_import_alias_mapping(node.names)
1089-
)
1090-
return self.add_module(
1085+
return self.add_module(
1086+
module=module,
1087+
module_or_package_name=None,
1088+
local_names=as_alias_handler(node.names),
1089+
import_alias_mapping=retrieve_import_alias_mapping(node.names),
1090+
from_from=True
1091+
)
1092+
for module in self.project_modules:
1093+
name = module[0]
1094+
if node.module == name:
1095+
if os.path.isdir(module[1]):
1096+
return self.from_directory_import(
10911097
module,
1092-
None,
1098+
not_as_alias_handler(node.names),
10931099
as_alias_handler(node.names),
1094-
retrieve_import_alias_mapping(node.names),
1095-
from_from=True
1100+
retrieve_import_alias_mapping(node.names)
10961101
)
1102+
return self.add_module(
1103+
module=module,
1104+
module_or_package_name=None,
1105+
local_names=as_alias_handler(node.names),
1106+
import_alias_mapping=retrieve_import_alias_mapping(node.names),
1107+
from_from=True
1108+
)
1109+
1110+
# Remember aliases for uninspectable modules such that we can label them fully qualified
1111+
# e.g. we want a call to "os.system" be recognised, even if we do "from os import system"
1112+
# from os import system as mysystem -> module=os, name=system, asname=mysystem
1113+
for name in node.names:
1114+
local_definitions = self.module_definitions_stack[-1]
1115+
local_definitions.import_alias_mapping[name.asname or name.name] = "{}.{}".format(node.module, name.name)
1116+
10971117
if node.module not in uninspectable_modules:
1098-
log.warn("Cannot inspect module %s", node.module)
1118+
log.warning("Cannot inspect module %s", node.module)
10991119
uninspectable_modules.add(node.module)
11001120
return IgnoredNode()

pyt/vulnerability_definitions/all_trigger_words.pyt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@
3131
]
3232
},
3333
"execute(": {},
34-
"system(": {},
34+
"os.system(": {},
3535
"filter(": {},
3636
"subprocess.call(": {},
37+
"subprocess.Popen(": {},
3738
"render_template(": {},
3839
"set_cookie(": {},
3940
"redirect(": {},
4041
"url_for(": {},
4142
"flash(": {},
4243
"jsonify(": {},
4344
"render(": {},
44-
"render_to_response(": {},
45-
"Popen(": {}
45+
"render_to_response(": {}
4646
}
4747
}

tests/main_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,11 @@ def test_targets_with_recursive(self):
108108
excluded_files = ""
109109

110110
included_files = discover_files(targets, excluded_files, True)
111-
self.assertEqual(len(included_files), 33)
111+
self.assertEqual(len(included_files), 34)
112112

113113
def test_targets_with_recursive_and_excluded(self):
114114
targets = ["examples/vulnerable_code/"]
115115
excluded_files = "inter_command_injection.py"
116116

117117
included_files = discover_files(targets, excluded_files, True)
118-
self.assertEqual(len(included_files), 32)
118+
self.assertEqual(len(included_files), 33)

tests/vulnerabilities/vulnerabilities_across_files_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def test_blackbox_library_call(self):
6262
EXPECTED_VULNERABILITY_DESCRIPTION = """
6363
File: examples/vulnerable_code_across_files/blackbox_library_call.py
6464
> User input at line 12, source "request.args.get(":
65-
~call_1 = ret_request.args.get('suggestion')
65+
~call_1 = ret_flask.request.args.get('suggestion')
6666
Reassigned in:
6767
File: examples/vulnerable_code_across_files/blackbox_library_call.py
6868
> Line 12: param = ~call_1

tests/vulnerabilities/vulnerabilities_base_test_case.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def string_compare_alpha(self, output, expected_string):
1111

1212
def assertAlphaEqual(self, output, expected_string):
1313
self.assertEqual(
14-
[char for char in output if char.isalpha()],
15-
[char for char in expected_string if char.isalpha()]
14+
''.join(char for char in output if char.isalpha()),
15+
''.join(char for char in expected_string if char.isalpha())
1616
)
1717
return True

0 commit comments

Comments
 (0)