python安全开发军规之一:防止shell注入

背景

在我们的日常开发中,经常会碰到需要使用系统能力的场景,这里的系统能力特指shell脚本,比如调用ping命令、结束进程等等。但是不怀好意的人可能会利用我们写的善良的程序来搞一些破坏。接下来我们就以ping命令为例讲解如何进行python的shell脚本安全开发。

普通程序员的写法
In [11]:
import subprocess
def subprocess_test(myserver):
    info=subprocess.check_output('ping %s' % myserver, shell=True)
    print(info.decode('GBK'))
    
subprocess_test('114.114.114.114')

正在 Ping 114.114.114.114 具有 32 字节的数据:

来自 114.114.114.114 的回复: 字节=32 时间=155ms TTL=69

来自 114.114.114.114 的回复: 字节=32 时间=93ms TTL=67

来自 114.114.114.114 的回复: 字节=32 时间=121ms TTL=68

来自 114.114.114.114 的回复: 字节=32 时间=131ms TTL=69



114.114.114.114 的 Ping 统计信息:

    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),

往返行程的估计时间(以毫秒为单位):

    最短 = 93ms,最长 = 155ms,平均 = 125ms


QA有话说

这个程序接受了变量为myserver的输入,执行了ping命令。但是输入和输出并没有做校验,check_output方法并不知道命令的起始位置是什么。
我们假定myserver这个变量是由用户输入的,则这段程序可被用来执行任何命令。

In [13]:
subprocess_test('114.114.114.114 && dir')

正在 Ping 114.114.114.114 具有 32 字节的数据:

来自 114.114.114.114 的回复: 字节=32 时间=44ms TTL=69

来自 114.114.114.114 的回复: 字节=32 时间=40ms TTL=69

来自 114.114.114.114 的回复: 字节=32 时间=36ms TTL=85

来自 114.114.114.114 的回复: 字节=32 时间=54ms TTL=65



114.114.114.114 的 Ping 统计信息:

    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),

往返行程的估计时间(以毫秒为单位):

    最短 = 36ms,最长 = 54ms,平均 = 43ms

 驱动器 D 中的卷是 数据

 卷的序列号是 000A-5C18



 D:\TestProj\test_python\zhyblog36\jupyter_data\c3_python_safety 的目录



2019/06/25  00:40    <DIR>          .

2019/06/25  00:40    <DIR>          ..

2019/06/25  00:10    <DIR>          .ipynb_checkpoints

2019/06/16  01:18             6,461 c3_001_python_safe_dpapi.ipynb

2019/06/25  00:40             5,774 Untitled.ipynb

               2 个文件         12,235 字节

               3 个目录  8,767,606,784 可用字节


QA使用了一个没破坏性的命令,如果是删除文件之类敏感的操作,则可能会带来灾难。

高级程序员的写法
In [18]:
def subprocess_test_safe(myserver):
    args=['ping',myserver]
    info=subprocess.check_output(args, shell=False)
    print(info.decode('GBK'))
    
subprocess_test_safe('114.114.114.114')
print('--------------------分割线----------------------')
subprocess_test_safe('114.114.114.114 && dir')

正在 Ping 114.114.114.114 具有 32 字节的数据:

来自 114.114.114.114 的回复: 字节=32 时间=137ms TTL=90

来自 114.114.114.114 的回复: 字节=32 时间=119ms TTL=61

来自 114.114.114.114 的回复: 字节=32 时间=134ms TTL=62

来自 114.114.114.114 的回复: 字节=32 时间=38ms TTL=90



114.114.114.114 的 Ping 统计信息:

    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),

往返行程的估计时间(以毫秒为单位):

    最短 = 38ms,最长 = 137ms,平均 = 107ms


--------------------分割线----------------------
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
<ipython-input-18-716d5b322d43> in <module>
      6 subprocess_test_safe('114.114.114.114')
      7 print('--------------------分割线----------------------')
----> 8 subprocess_test_safe('114.114.114.114 && dir')

<ipython-input-18-716d5b322d43> in subprocess_test_safe(myserver)
      1 def subprocess_test_safe(myserver):
      2     args=['ping',myserver]
----> 3     info=subprocess.check_output(args, shell=False)
      4     print(info.decode('GBK'))
      5 

d:\program\python\python36\Lib\subprocess.py in check_output(timeout, *popenargs, **kwargs)
    334 
    335     return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
--> 336                **kwargs).stdout
    337 
    338 

d:\program\python\python36\Lib\subprocess.py in run(input, timeout, check, *popenargs, **kwargs)
    416         if check and retcode:
    417             raise CalledProcessError(retcode, process.args,
--> 418                                      output=stdout, stderr=stderr)
    419     return CompletedProcess(process.args, retcode, stdout, stderr)
    420 

CalledProcessError: Command '['ping', '114.114.114.114 && dir']' returned non-zero exit status 1.

通过将参数列表化,而不是直接将字符串传递给子进程,ping命令分别获取每个参数,因此Shell不会在ping命令执行结束后执行其它命令。这里的Shell默认为False,如果将其设置为True,将会更容易受到攻击。