Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /