1# Copyright 2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15################################################################################
16"""Sanitizers for capturing code injections."""
17
18from typing import Optional
19from pysecsan import sanlib
20
21
22def get_all_substr_prefixes(main_str, sub_str):
23 """Yields all strings prefixed with sub_str in main_str."""
24 idx = 0
25 while True:
26 idx = main_str.find(sub_str, idx)
27 if idx == -1:
28 return
29 yield main_str[0:idx]
30 # Increase idx the length of the substring from the current position
31 # where an occurence of the substring was found.
32 idx += len(sub_str)
33
34
35# pylint: disable=unsubscriptable-object
36def check_code_injection_match(elem, check_unquoted=False) -> Optional[str]:
37 """identify if elem is an injection match."""
38 # Check exact match
39 if elem == 'exec-sanitizer':
40 return 'Explicit command injection found.'
41
42 # Check potential for injecting into a string
43 if 'FROMFUZZ' in elem:
44 if check_unquoted:
45 # return true if any index is unquoted
46 for sub_str in get_all_substr_prefixes(elem, 'FROMFUZZ'):
47 if sub_str.count('\"') % 2 == 0:
48 return 'Fuzzer controlled content in data. Code injection potential.'
49
50 # Return None if all fuzzer taints were quoted
51 return None
52 return 'Fuzzer-controlled data in command string. Injection potential.'
53 return None
54
55
56# pylint: disable=invalid-name
57def hook_pre_exec_subprocess_Popen(cmd, **kwargs):
58 """Hook for subprocess.Popen."""
59
60 arg_shell = 'shell' in kwargs and kwargs['shell']
61
62 # Command injections depend on whether the first argument is a list of
63 # strings or a string. Handle this now.
64 # Example: tests/poe/ansible-runner-cve-2021-4041
65 if isinstance(cmd, str):
66 res = check_code_injection_match(cmd, check_unquoted=True)
67 if res is not None:
68 # if shell arg is true and string is tainted and unquoted that's a
69 # definite code injection.
70 if arg_shell is True:
71 sanlib.abort_with_issue('Code injection in Popen', 'Command injection')
72
73 # It's a maybe: will not report this to avoid false positives.
74 # TODO: add more precise detection here.
75
76 # Check for hg command injection
77 # Example: tests/poe/libvcs-cve-2022-21187
78 if cmd[0] == 'hg':
79 # Check if the arguments are controlled by the fuzzer, and this given
80 # arg is not preceded by --
81 found_dashes = False
82 for idx in range(1, len(cmd)):
83 if cmd[0] == '--':
84 found_dashes = True
85 if not found_dashes and check_code_injection_match(cmd[idx]):
86 sanlib.abort_with_issue(
87 'command injection likely by way of mercurial. The following'
88 f'command {str(cmd)} is executed, and if you substitute {cmd[idx]} '
89 'with \"--config=alias.init=!touch HELLO_PY\" then you will '
90 'create HELLO_PY', 'Command injection')
91
92
93def hook_pre_exec_os_system(cmd):
94 """Hook for os.system."""
95 res = check_code_injection_match(cmd)
96 if res is not None:
97 sanlib.abort_with_issue(f'code injection by way of os.system\n{res}',
98 'Command injection')
99
100
101def hook_pre_exec_eval(cmd, *args, **kwargs):
102 """Hook for eval. Experimental atm."""
103 res = check_code_injection_match(cmd, check_unquoted=True)
104 if res is not None:
105 sanlib.abort_with_issue(f'Potential code injection by way of eval\n{res}',
106 'Command injection')