Python高级技术之:`Python`的`subprocess`模块:如何安全地执行外部命令。

各位好,今天咱们来聊聊Python里的“特工”模块:subprocess。 想象一下,你的Python程序想要指挥电脑干点儿别的事情,比如运行个系统命令、调用个外部程序啥的。 这时候,subprocess就派上大用场了。 但要注意,用不好这个“特工”可是会出事的!所以咱们得好好学学怎么安全地使用它。

一、subprocess是个啥?

简单来说,subprocess模块允许你创建新的进程,连接到它们的输入/输出/错误管道,并获取它们的返回码。 它可以用来执行各种外部命令,比如:

  • 运行shell命令 (比如 ls, dir, grep 等)
  • 执行其他Python脚本
  • 启动其他应用程序 (比如文本编辑器、浏览器等)

二、subprocess模块的核心函数

subprocess模块里有很多函数,但最核心的几个是:

  • subprocess.run():这是Python 3.5之后推荐使用的方法,它会运行命令,等待命令完成,然后返回一个CompletedProcess对象,包含了命令的执行结果。
  • subprocess.Popen():这是更底层的接口,它允许你更细粒度地控制子进程的创建和交互。 它会立即返回一个Popen对象,你可以用它来控制子进程的输入/输出/错误,并等待子进程完成。
  • subprocess.call()subprocess.check_call()subprocess.check_output():这些是更早期的函数,现在已经不推荐使用了,因为它们的功能比较有限,而且安全性也相对较差。

三、subprocess.run():简单好用,安全第一

先来看看subprocess.run(),它是最简单也是最安全的用法。

import subprocess

# 运行一个简单的命令,比如查看当前目录下的文件列表
result = subprocess.run(['ls', '-l'], capture_output=True, text=True)

# 检查命令是否成功执行
if result.returncode == 0:
    print("命令执行成功!")
    print("输出结果:")
    print(result.stdout)
else:
    print("命令执行失败!")
    print("错误信息:")
    print(result.stderr)

这段代码做了什么?

  1. subprocess.run(['ls', '-l'], ...): 调用subprocess.run()函数,第一个参数是一个列表,列表的第一个元素是要执行的命令,后面的元素是命令的参数。 ls -l 这个命令在 Linux 和 macOS 系统下会列出当前目录下的文件和目录的详细信息。
  2. capture_output=True: 这个参数告诉subprocess捕获命令的输出结果(包括标准输出和标准错误)。 如果不设置这个参数,命令的输出会直接打印到控制台,而不是被Python程序捕获。
  3. text=True: 这个参数告诉subprocess以文本模式处理输出结果。 默认情况下,subprocess会以字节模式处理输出结果,你需要手动解码成字符串。 设置text=True之后,subprocess会自动帮你解码成字符串。
  4. result.returncode: 这是命令的返回码。 返回码为0表示命令执行成功,非0表示命令执行失败。
  5. result.stdout: 这是命令的标准输出结果。
  6. result.stderr: 这是命令的标准错误结果。

安全提示:避免shell=True

subprocess.run()有一个参数叫做shell=True, 看起来好像很方便,可以直接执行shell命令,比如:

import subprocess

# 这样写很方便,但很危险!
result = subprocess.run('ls -l', capture_output=True, text=True, shell=True)

千万不要这么做! 除非你完全信任你要执行的命令的来源。 shell=True会调用系统的shell来执行命令,这意味着你的程序可能会受到shell注入攻击。 比如,如果你的命令是从用户输入获取的,那么恶意用户可以通过构造恶意的命令来执行任意代码。

为什么shell=True不安全?

因为当 shell=True 时,你的命令字符串会被传递给 shell 解释器(比如 Bash)。 Shell 解释器会先对这个字符串进行解析,然后再执行解析后的命令。 这就给了恶意用户可乘之机,他们可以在命令字符串中插入恶意的 shell 命令,让你的程序执行他们想要执行的任何操作。

举个例子,假设你的程序需要执行一个命令来查找包含特定字符串的文件:

import subprocess

filename = input("请输入文件名:")  # 假设用户输入了文件名
command = "grep " + filename + " file.txt"  # 构造命令字符串

result = subprocess.run(command, capture_output=True, text=True, shell=True) # 执行命令

如果恶意用户输入了 "; rm -rf /" 作为文件名,那么构造出来的命令字符串就会变成:

grep "; rm -rf /" file.txt

shell=True 时,shell 解释器会先执行 grep 命令,然后执行 rm -rf / 命令,这将删除你的整个文件系统!

如何避免shell=True的风险?

永远不要使用 shell=True,除非你完全信任你要执行的命令的来源。 如果你需要执行复杂的 shell 命令,可以考虑使用 subprocess.Popen() 来更细粒度地控制子进程的创建和交互,或者使用 shlex.split() 函数来将命令字符串分割成一个列表,然后再传递给 subprocess.run()

正确的做法:使用列表传递参数

import subprocess

# 这样写更安全
command = ['ls', '-l'] # 把命令和参数放在一个列表里
result = subprocess.run(command, capture_output=True, text=True)

这样,subprocess会直接调用ls命令,而不会经过shell的解析,避免了shell注入的风险。

四、subprocess.Popen():灵活控制,小心翼翼

subprocess.Popen()提供了更底层的接口,让你能够更灵活地控制子进程的创建和交互。 但也意味着你需要自己处理更多的细节,比如输入/输出/错误的重定向、进程的等待等。

import subprocess

# 创建一个子进程,执行ls -l命令
process = subprocess.Popen(['ls', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# 获取子进程的输出和错误
stdout, stderr = process.communicate()

# 等待子进程结束
return_code = process.wait()

# 检查命令是否成功执行
if return_code == 0:
    print("命令执行成功!")
    print("输出结果:")
    print(stdout)
else:
    print("命令执行失败!")
    print("错误信息:")
    print(stderr)

这段代码做了什么?

  1. subprocess.Popen(['ls', '-l'], ...): 创建一个Popen对象,启动一个子进程来执行ls -l命令。
  2. stdout=subprocess.PIPE: 将子进程的标准输出重定向到管道。
  3. stderr=subprocess.PIPE: 将子进程的标准错误重定向到管道。
  4. process.communicate(): 从管道中读取子进程的输出和错误。 这个方法会阻塞,直到子进程结束。
  5. process.wait(): 等待子进程结束,并获取子进程的返回码。

Popen对象的常用方法

  • Popen.communicate(input=None, timeout=None): 与子进程进行交互。 可以向子进程发送数据(通过input参数),并获取子进程的输出和错误。
  • Popen.poll(): 检查子进程是否已经结束。 如果子进程已经结束,返回子进程的返回码;否则返回None
  • Popen.wait(timeout=None): 等待子进程结束,并获取子进程的返回码。
  • Popen.send_signal(signal): 向子进程发送信号。
  • Popen.terminate(): 终止子进程。
  • Popen.kill(): 强制终止子进程。
  • Popen.pid: 子进程的进程ID。
  • Popen.stdin: 子进程的标准输入。
  • Popen.stdout: 子进程的标准输出。
  • Popen.stderr: 子进程的标准错误。

安全提示:输入验证和清理

在使用subprocess.Popen()时,一定要对输入进行验证和清理,避免命令注入攻击。 即使你没有使用shell=True,恶意用户仍然可以通过构造恶意的输入来影响子进程的行为。

比如,如果你的程序需要根据用户输入的文件名来读取文件内容:

import subprocess
import shlex

filename = input("请输入文件名:")

# 对文件名进行验证和清理
if not filename.isalnum():
    print("文件名只能包含字母和数字!")
    exit()

command = ['cat', filename]

process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = process.communicate()

if process.returncode == 0:
    print(stdout)
else:
    print(stderr)

这段代码对文件名进行了验证,确保文件名只包含字母和数字。 这样可以避免恶意用户输入包含特殊字符的文件名,从而避免命令注入攻击。

五、输入输出重定向

subprocess模块提供了多种方式来重定向子进程的输入/输出/错误。

  • subprocess.PIPE: 创建一个管道,用于连接父进程和子进程的输入/输出/错误。
  • subprocess.DEVNULL: 将子进程的输入/输出/错误重定向到空设备,相当于丢弃所有的数据。
  • open(): 可以使用open()函数打开一个文件,然后将子进程的输入/输出/错误重定向到这个文件。

示例:将子进程的输出重定向到文件

import subprocess

with open("output.txt", "w") as outfile:
    result = subprocess.run(['ls', '-l'], stdout=outfile)

这段代码将ls -l命令的输出重定向到output.txt文件中。

示例:将子进程的错误重定向到标准输出

import subprocess

result = subprocess.run(['ls', '-l', 'nonexistent_file'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)

print(result.stdout)

这段代码将ls -l nonexistent_file命令的错误重定向到标准输出,然后将标准输出打印到控制台。

六、超时处理

如果子进程运行时间过长,可能会导致你的程序阻塞。 为了避免这种情况,你可以设置超时时间。

  • subprocess.run(): 可以使用timeout参数设置超时时间(单位:秒)。 如果子进程在指定的时间内没有结束,subprocess.run()会抛出一个TimeoutExpired异常。
  • Popen.communicate(): 也可以使用timeout参数设置超时时间。 如果子进程在指定的时间内没有返回输出,Popen.communicate()会抛出一个TimeoutExpired异常。

示例:使用subprocess.run()设置超时时间

import subprocess

try:
    result = subprocess.run(['sleep', '10'], timeout=5)
except subprocess.TimeoutExpired:
    print("命令执行超时!")

这段代码会尝试执行sleep 10命令,但是设置了5秒的超时时间。 如果sleep 10命令在5秒内没有结束,subprocess.run()会抛出一个TimeoutExpired异常。

示例:使用Popen.communicate()设置超时时间

import subprocess

process = subprocess.Popen(['sleep', '10'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

try:
    stdout, stderr = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
    process.kill() # 杀死子进程
    print("命令执行超时!")

这段代码和上面的代码类似,但是使用了Popen.communicate()来设置超时时间。 如果sleep 10命令在5秒内没有返回输出,Popen.communicate()会抛出一个TimeoutExpired异常,并且会杀死子进程。

七、总结

subprocess模块是Python中一个非常强大的工具,可以用来执行外部命令。 但是,在使用subprocess模块时,一定要注意安全性,避免命令注入攻击。

  • 永远不要使用shell=True,除非你完全信任你要执行的命令的来源。
  • 对输入进行验证和清理,避免恶意用户构造恶意的输入。
  • 设置超时时间,避免子进程运行时间过长导致程序阻塞。

希望今天的分享对大家有所帮助! 记住,安全第一,玩转subprocess,让你的Python程序更上一层楼!下次有机会再和大家聊聊其他有趣的技术话题。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注