Skip to content

Commit 33d665c

Browse files
authored
replace shell=True, replace xml, and replace exit() (sonic-net#2664)
Signed-off-by: maipbui <[email protected]> #### What I did The [xml.etree.ElementTree](https://docs.python.org/3/library/xml.etree.elementtree.html#module-xml.etree.ElementTree) module is not secure against maliciously constructed data. `subprocess()` - when using with `shell=True` is dangerous. Using subprocess function without a static string can lead to command injection. `sys.exit` is better than `exit`, considered good to use in production code. Ref: https://stackoverflow.com/questions/6501121/difference-between-exit-and-sys-exit-in-python https://stackoverflow.com/questions/19747371/python-exit-commands-why-so-many-and-when-should-each-be-used #### How I did it Remove xml. Use [lxml](https://pypi.org/project/lxml/) XML parsers package that prevent potentially malicious operation. `subprocess()` - use `shell=False` instead, use list of strings Ref: [https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation](https://semgrep.dev/docs/cheat-sheets/python-command-injection/#mitigation) Replace `exit()` by `sys.exit()` #### How to verify it Pass UT Manual test
1 parent 9e510a8 commit 33d665c

17 files changed

+204
-86
lines changed

pfcwd/main.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def start(self, action, restoration_time, ports, detection_time):
243243
click.echo("Failed to run command, invalid options:")
244244
for opt in invalid_ports:
245245
click.echo(opt)
246-
exit(1)
246+
sys.exit(1)
247247
self.start_cmd(action, restoration_time, ports, detection_time)
248248

249249

@@ -263,7 +263,7 @@ def verify_pfc_enable_status_per_port(self, port, pfcwd_info):
263263
@multi_asic_util.run_on_multi_asic
264264
def start_cmd(self, action, restoration_time, ports, detection_time):
265265
if os.geteuid() != 0:
266-
exit("Root privileges are required for this operation")
266+
sys.exit("Root privileges are required for this operation")
267267

268268
all_ports = get_all_ports(
269269
self.db, self.multi_asic.current_namespace,
@@ -299,7 +299,7 @@ def start_cmd(self, action, restoration_time, ports, detection_time):
299299
@multi_asic_util.run_on_multi_asic
300300
def interval(self, poll_interval):
301301
if os.geteuid() != 0:
302-
exit("Root privileges are required for this operation")
302+
sys.exit("Root privileges are required for this operation")
303303
pfcwd_info = {}
304304
if poll_interval is not None:
305305
pfcwd_table = self.config_db.get_table(CONFIG_DB_PFC_WD_TABLE_NAME)
@@ -331,7 +331,7 @@ def interval(self, poll_interval):
331331
poll_interval, entry_min_str
332332
), err=True
333333
)
334-
exit(1)
334+
sys.exit(1)
335335

336336
pfcwd_info['POLL_INTERVAL'] = poll_interval
337337
self.config_db.mod_entry(
@@ -341,7 +341,7 @@ def interval(self, poll_interval):
341341
@multi_asic_util.run_on_multi_asic
342342
def stop(self, ports):
343343
if os.geteuid() != 0:
344-
exit("Root privileges are required for this operation")
344+
sys.exit("Root privileges are required for this operation")
345345

346346
all_ports = get_all_ports(
347347
self.db, self.multi_asic.current_namespace,
@@ -359,7 +359,7 @@ def stop(self, ports):
359359
@multi_asic_util.run_on_multi_asic
360360
def start_default(self):
361361
if os.geteuid() != 0:
362-
exit("Root privileges are required for this operation")
362+
sys.exit("Root privileges are required for this operation")
363363
enable = self.config_db.get_entry('DEVICE_METADATA', 'localhost').get(
364364
'default_pfcwd_status'
365365
)
@@ -394,15 +394,15 @@ def start_default(self):
394394
@multi_asic_util.run_on_multi_asic
395395
def counter_poll(self, counter_poll):
396396
if os.geteuid() != 0:
397-
exit("Root privileges are required for this operation")
397+
sys.exit("Root privileges are required for this operation")
398398
pfcwd_info = {}
399399
pfcwd_info['FLEX_COUNTER_STATUS'] = counter_poll
400400
self.config_db.mod_entry("FLEX_COUNTER_TABLE", "PFCWD", pfcwd_info)
401401

402402
@multi_asic_util.run_on_multi_asic
403403
def big_red_switch(self, big_red_switch):
404404
if os.geteuid() != 0:
405-
exit("Root privileges are required for this operation")
405+
sys.exit("Root privileges are required for this operation")
406406
pfcwd_info = {}
407407
if big_red_switch is not None:
408408
pfcwd_info['BIG_RED_SWITCH'] = big_red_switch

scripts/boot_part

+19-9
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import re
55
import os
66
import sys
7-
import commands
87
import argparse
98
import logging
109
import tempfile
10+
from sonic_py_common.general import getstatusoutput_noshell
1111

1212
logging.basicConfig(level=logging.WARN)
1313
logger = logging.getLogger(__name__)
@@ -20,7 +20,7 @@ re_hex = r'[0-9A-F]'
2020
## String - the standard output of the command, may be empty string
2121
def runcmd(cmd):
2222
logger.info('runcmd: {0}'.format(cmd))
23-
rc, out = commands.getstatusoutput(cmd)
23+
rc, out = getstatusoutput_noshell(cmd)
2424
if rc == 0:
2525
return out
2626
else:
@@ -29,7 +29,7 @@ def runcmd(cmd):
2929

3030
def print_partitions(blkdev):
3131
assert blkdev
32-
out = runcmd('sudo lsblk -r -o PARTLABEL,NAME')
32+
out = runcmd(['sudo', 'lsblk', '-r', '-o', 'PARTLABEL,NAME'])
3333
## Parse command output and print
3434
found_table = False
3535
for line in out.splitlines():
@@ -56,7 +56,7 @@ def print_partitions(blkdev):
5656

5757
## Get the current boot partition index
5858
def get_boot_partition(blkdev):
59-
out = runcmd('cat /proc/mounts')
59+
out = runcmd(['cat', '/proc/mounts'])
6060
if out is None: return None
6161

6262
## Parse command output and return the current boot partition index
@@ -76,16 +76,26 @@ def set_boot_partition(blkdev, index):
7676
devnode = blkdev + str(index)
7777
mntpath = tempfile.mkdtemp()
7878
try:
79-
out = runcmd('sudo mount {0} {1}'.format(devnode, mntpath))
79+
out = runcmd(['sudo', 'mount', devnode, mntpath])
8080
logger.info('mount out={0}'.format(out))
8181
if out is None: return
8282
## Set GRUB bootable
83-
out = runcmd('sudo grub-install --boot-directory="{0}" --recheck "{1}"'.format(mntpath, blkdev))
83+
out = runcmd(['sudo', 'grub-install', '--boot-directory='+mntpath, "--recheck", blkdev])
8484
return out is not None
8585
finally:
8686
## Cleanup
87-
out = runcmd('sudo fuser -km {0} || sudo umount {0}'.format(mntpath))
88-
logger.info('fuser out={0}'.format(out))
87+
cmd1 = ['sudo', 'fuser', '-km', mntpath]
88+
rc1, out1 = getstatusoutput_noshell(cmd1)
89+
if rc1 != 0:
90+
logger.error('Failed to run: {0}\n{1}'.format(cmd1, out1))
91+
cmd2 = ['sudo', 'unmount', mntpath]
92+
rc2, out2 = getstatusoutput_noshell(cmd2)
93+
if rc2 == 0:
94+
logger.info('Running command: {0}\n{1}'.format(' '.join(cmd2), out2))
95+
else:
96+
logger.error('Failed to run: {0}\n{1}'.format(cmd2, out2))
97+
else:
98+
logger.info('Running command: {0}\n{1}'.format(' '.join(cmd1), out1))
8999
os.rmdir(mntpath)
90100

91101
def main():
@@ -100,7 +110,7 @@ def main():
100110
logger.setLevel(logging.INFO)
101111

102112
## Find ONIE partition and get the block device containing ONIE
103-
out = runcmd("sudo blkid")
113+
out = runcmd(["sudo", "blkid"])
104114
if not out: return -1
105115
for line in out.splitlines():
106116
m = re.match(r'/dev/(\w+)\d+: LABEL="ONIE-BOOT"', line)

scripts/check_db_integrity.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ def main():
3636

3737
for db_name, schema in DB_SCHEMA.items():
3838
db_dump_file = "/tmp/{}.json".format(db_name)
39-
dump_db_cmd = "sonic-db-dump -n 'COUNTERS_DB' -y > {}".format(db_dump_file)
40-
p = subprocess.Popen(dump_db_cmd, shell=True, text=True,
41-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
39+
dump_db_cmd = ["sonic-db-dump", "-n", 'COUNTERS_DB', "-y"]
40+
with open(db_dump_file, 'w') as f:
41+
p = subprocess.Popen(dump_db_cmd, text=True, stdout=f, stderr=subprocess.PIPE)
4242
(_, err) = p.communicate()
4343
rc = p.wait()
4444
if rc != 0:

scripts/configlet

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/usr/bin/env python3
2+
3+
import sys
4+
25
""" JSON based configlet update
36
47
A tool to update CONFIG-DB with JSON diffs that can update/delete redis-DB.
@@ -195,7 +198,7 @@ def main():
195198
if not do_act:
196199
print("Expect an action update/delete or for debug parse/test\n")
197200
parser.print_help()
198-
exit(-1)
201+
sys.exit(-1)
199202

200203
for json_file in args.json:
201204
with open(json_file, 'r') as stream:

scripts/disk_check.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def test_writable(dirs):
8383

8484

8585
def run_cmd(cmd):
86-
proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)
86+
proc = subprocess.run(cmd, shell=False, stdout=subprocess.PIPE)
8787
ret = proc.returncode
8888
if ret:
8989
log_err("failed: ret={} cmd={}".format(ret, cmd))
@@ -120,9 +120,8 @@ def do_mnt(dirs):
120120
os.mkdir(d_upper)
121121
os.mkdir(d_work)
122122

123-
ret = run_cmd("mount -t overlay overlay_{} -o lowerdir={},"
124-
"upperdir={},workdir={} {}".format(
125-
d_name, d, d_upper, d_work, d))
123+
ret = run_cmd(["mount", "-t", "overlay", "overlay_{}".format(d_name),\
124+
"-o", "lowerdir={},upperdir={},workdir={}".format(d, d_upper, d_work), d])
126125
if ret:
127126
break
128127

scripts/dropconfig

+4-4
Original file line numberDiff line numberDiff line change
@@ -375,25 +375,25 @@ Examples:
375375
reasons)
376376
except InvalidArgumentError as err:
377377
print('Encountered error trying to install counter: {}'.format(err.message))
378-
exit(1)
378+
sys.exit(1)
379379
elif command == 'uninstall':
380380
try:
381381
dconfig.delete_counter(name)
382382
except InvalidArgumentError as err:
383383
print('Encountered error trying to uninstall counter: {}'.format(err.message))
384-
exit(1)
384+
sys.exit(1)
385385
elif command == 'add':
386386
try:
387387
dconfig.add_reasons(name, reasons)
388388
except InvalidArgumentError as err:
389389
print('Encountered error trying to add reasons: {}'.format(err.message))
390-
exit(1)
390+
sys.exit(1)
391391
elif command == 'remove':
392392
try:
393393
dconfig.remove_reasons(name, reasons)
394394
except InvalidArgumentError as err:
395395
print('Encountered error trying to remove reasons: {}'.format(err.message))
396-
exit(1)
396+
sys.exit(1)
397397
elif command == 'show_config':
398398
dconfig.print_counter_config(group)
399399
elif command == 'show_capabilities':

scripts/dump_nat_entries.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import subprocess
99

1010
def main():
11-
ctdumpcmd = 'conntrack -L -j > /host/warmboot/nat/nat_entries.dump'
12-
p = subprocess.Popen(ctdumpcmd, shell=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
11+
ctdumpcmd = ['conntrack', '-L', '-j']
12+
file = '/host/warmboot/nat/nat_entries.dump'
13+
with open(file, 'w') as f:
14+
p = subprocess.Popen(ctdumpcmd, text=True, stdout=f, stderr=subprocess.PIPE)
1315
(output, err) = p.communicate()
1416
rc = p.wait()
1517

scripts/ipintutil

+4-6
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,12 @@ def get_if_admin_state(iface, namespace):
7272
"""
7373
Given an interface name, return its admin state reported by the kernel
7474
"""
75-
cmd = "cat /sys/class/net/{0}/flags".format(iface)
75+
cmd = ["cat", "/sys/class/net/{0}/flags".format(iface)]
7676
if namespace != constants.DEFAULT_NAMESPACE:
77-
cmd = "sudo ip netns exec {} {}".format(namespace, cmd)
77+
cmd = ["sudo", "ip", "netns", "exec", namespace] + cmd
7878
try:
7979
proc = subprocess.Popen(
8080
cmd,
81-
shell=True,
8281
stderr=subprocess.STDOUT,
8382
stdout=subprocess.PIPE,
8483
text=True)
@@ -105,13 +104,12 @@ def get_if_oper_state(iface, namespace):
105104
"""
106105
Given an interface name, return its oper state reported by the kernel.
107106
"""
108-
cmd = "cat /sys/class/net/{0}/carrier".format(iface)
107+
cmd = ["cat", "/sys/class/net/{0}/carrier".format(iface)]
109108
if namespace != constants.DEFAULT_NAMESPACE:
110-
cmd = "sudo ip netns exec {} {}".format(namespace, cmd)
109+
cmd = ["sudo", "ip", "netns", "exec", namespace] + cmd
111110
try:
112111
proc = subprocess.Popen(
113112
cmd,
114-
shell=True,
115113
stderr=subprocess.STDOUT,
116114
stdout=subprocess.PIPE,
117115
text=True)

scripts/lldpshow

+10-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import argparse
2323
import re
2424
import subprocess
2525
import sys
26-
import xml.etree.ElementTree as ET
26+
from lxml import etree as ET
2727

2828
from sonic_py_common import device_info
2929
from swsscommon.swsscommon import ConfigDBConnector
@@ -80,9 +80,14 @@ class Lldpshow(object):
8080
lldp_interface_list = lldp_port if lldp_port is not None else self.lldp_interface[lldp_instace_num]
8181
# In detail mode we will pass interface list (only front ports) and get O/P as plain text
8282
# and in table format we will get xml output
83-
lldp_cmd = 'sudo docker exec -i lldp{} lldpctl '.format(self.lldp_instance[lldp_instace_num]) + (
84-
'-f xml' if not lldp_detail_info else lldp_interface_list)
85-
p = subprocess.Popen(lldp_cmd, stdout=subprocess.PIPE, shell=True, text=True)
83+
if not lldp_detail_info:
84+
lldp_args = ['-f', 'xml']
85+
elif lldp_interface_list == '':
86+
lldp_args = []
87+
else:
88+
lldp_args = [lldp_interface_list]
89+
lldp_cmd = ['sudo', 'docker', 'exec', '-i', 'lldp{}'.format(self.lldp_instance[lldp_instace_num]), 'lldpctl'] + lldp_args
90+
p = subprocess.Popen(lldp_cmd, stdout=subprocess.PIPE, text=True)
8691
(output, err) = p.communicate()
8792
## Wait for end of command. Get return returncode ##
8893
returncode = p.wait()
@@ -121,7 +126,7 @@ class Lldpshow(object):
121126
if lldp_detail_info:
122127
return
123128
for lldpraw in self.lldpraw:
124-
neis = ET.fromstring(lldpraw)
129+
neis = ET.fromstring(lldpraw.encode())
125130
intfs = neis.findall('interface')
126131
for intf in intfs:
127132
l_intf = intf.attrib['name']

scripts/storyteller

+15-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import subprocess
1111
import sys
1212

1313
from shlex import quote
14+
from sonic_py_common.general import getstatusoutput_noshell_pipe
1415

1516
regex_dict = {
1617
'acl' : r'acl\|ACL\|Acl',
@@ -28,31 +29,35 @@ reference_file = '/tmp/storyteller_time_reference'
2829

2930
def exec_cmd(cmd):
3031
# Use universal_newlines (instead of text) so that this tool can work with any python versions.
31-
out = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, universal_newlines=True)
32+
out = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
3233
stdout, stderr = out.communicate()
3334
return out.returncode, stdout, stderr
3435

3536

3637
def build_options(after=0, before=0, context=0):
3738
options = []
3839
if after:
39-
options.append('-A {}'.format(after))
40+
options += ['-A', str(after)]
4041
if before:
41-
options.append('-B {}'.format(before))
42+
options += ['-B', str(before)]
4243
if context:
43-
options.append('-C {}'.format(context))
44+
options += ['-C', str(context)]
4445

45-
return ' '.join(x for x in options)
46+
return options
4647

4748

4849
def find_log(logpath, log, regex, after=0, before=0, context=0, field=0):
4950
options = build_options(after, before, context)
5051
if field <= 0:
51-
cmd = 'find -L {}/{}* -newer {} | xargs ls -rt | xargs zgrep -a {} "{}"'.format(logpath, log, reference_file, options, regex)
52+
cmd0 = ['find', logpath, "-name", "{}*".format(log), "-newer", reference_file]
53+
cmd1 = ["xargs", "ls", "-rt"]
54+
cmd2 = ["xargs", "zgrep", "-a"] + options + [regex]
5255
else:
53-
cmd = 'find -L {0}/{1}* -newer {2} | sort -rn -t . -k {3},{3} | xargs zgrep -a {4} "{5}"'.format(logpath, log, reference_file, field, options, regex)
56+
cmd0 = ['find', logpath, "-name", "{}*".format(log), "-newer", reference_file]
57+
cmd1 = ["sort", "-rn", "-t", ".", "-k", "{0},{0}".format(field)]
58+
cmd2 = ["xargs", "zgrep", "-a"] + options + [regex]
5459

55-
_, out, _ = exec_cmd(cmd)
60+
_, out = getstatusoutput_noshell_pipe(cmd0, cmd1, cmd2)
5661
'''
5762
Opportunity to improve:
5863
output (out) can be split to lines and send to a filter to
@@ -71,12 +76,12 @@ def build_regex(category):
7176

7277

7378
def configure_time_filter(since):
74-
ret_code, _, _ = exec_cmd('date --date {}'.format(since))
79+
ret_code, _, _ = exec_cmd(['date', '--date', since])
7580
if ret_code:
7681
print('invalid date "{}"'.format(since))
7782
sys.exit(1)
7883

79-
exec_cmd('touch --date "{}" {}'.format(since, reference_file))
84+
exec_cmd(['touch', '--date', since, reference_file])
8085

8186

8287
def main():

0 commit comments

Comments
 (0)