Skip to content

Commit 40c1f24

Browse files
committed
Resolve aliases for black box and built-in function calls.
* Allow trigger words to be fully qualified to reduce false positives
1 parent 3b344a3 commit 40c1f24

File tree

6 files changed

+73
-9
lines changed

6 files changed

+73
-9
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
os.system("ls")
8+
myos.system("ls")
9+
system("ls")
10+
mysystem("ls")
11+
mycall("ls")
12+
mypopen("ls")

pyt/cfg/alias_helper.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,22 @@ 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+
if label.startswith(alias+'.'):
93+
return full_name + label[len(alias):]
94+
return label
95+

pyt/cfg/stmt_visitor.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
handle_aliases_in_init_files,
1010
handle_fdid_aliases,
1111
not_as_alias_handler,
12-
retrieve_import_alias_mapping
12+
retrieve_import_alias_mapping,
13+
fully_qualify_alias_labels
1314
)
1415
from ..core.ast_helper import (
1516
generate_ast,
@@ -61,6 +62,7 @@
6162
class StmtVisitor(ast.NodeVisitor):
6263
def __init__(self, allow_local_directory_imports=True):
6364
self._allow_local_modules = allow_local_directory_imports
65+
self.bb_or_bi_aliases = {}
6466
super().__init__()
6567

6668
def visit_Module(self, node):
@@ -624,6 +626,10 @@ def add_blackbox_or_builtin_call(self, node, blackbox): # noqa: C901
624626

625627
call_function_label = call_label_visitor.result[:call_label_visitor.result.find('(')]
626628

629+
# Check if function call matches a blackbox/built-in alias and if so, resolve it
630+
# This resolves aliases like "from os import system as mysys" as: mysys -> os.system
631+
call_function_label = fully_qualify_alias_labels(call_function_label, self.bb_or_bi_aliases)
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,6 @@ 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
814819
parent_definitions.import_alias_mapping.update(import_alias_mapping)
815820
parent_definitions.import_names = local_names
816821

@@ -1052,7 +1057,13 @@ def visit_Import(self, node):
10521057
retrieve_import_alias_mapping(node.names)
10531058
)
10541059
for alias in node.names:
1055-
if alias.name not in uninspectable_modules:
1060+
if alias.name in uninspectable_modules:
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+
self.bb_or_bi_aliases[alias.asname] = alias.name
1066+
else:
10561067
log.warn("Cannot inspect module %s", alias.name)
10571068
uninspectable_modules.add(alias.name) # Don't repeatedly warn about this
10581069
return IgnoredNode()
@@ -1094,7 +1105,14 @@ def visit_ImportFrom(self, node):
10941105
retrieve_import_alias_mapping(node.names),
10951106
from_from=True
10961107
)
1097-
if node.module not in uninspectable_modules:
1108+
1109+
if node.module in uninspectable_modules:
1110+
# Remember aliases for blackboxed and built-in imports 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+
self.bb_or_bi_aliases[name.asname or name.name] = "{}.{}".format(node.module, name.name)
1115+
else:
10981116
log.warn("Cannot inspect module %s", node.module)
10991117
uninspectable_modules.add(node.module)
11001118
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_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,21 @@ def test_list_append_taints_list(self):
482482
)
483483
self.assert_length(vulnerabilities, expected_length=1)
484484

485+
def test_import_bb_or_bi_with_alias(self):
486+
self.cfg_create_from_file('examples/vulnerable_code/command_injection_with_aliases.py')
487+
488+
EXPECTED = ['Entry module',
489+
"~call_1 = ret_os.system('ls')",
490+
"~call_2 = ret_os.system('ls')",
491+
"~call_3 = ret_os.system('ls')",
492+
"~call_4 = ret_os.system('ls')",
493+
"~call_5 = ret_subprocess.call('ls')",
494+
"~call_6 = ret_subprocess.Popen('ls')",
495+
'Exit module'
496+
]
497+
for node, expected_label in zip(self.cfg.nodes, EXPECTED):
498+
self.assertEqual(node.label, expected_label)
499+
485500

486501
class EngineDjangoTest(VulnerabilitiesBaseTestCase):
487502
def run_analysis(self, path):

0 commit comments

Comments
 (0)