为什么直接用变量会有风险?
当你尝试像下面这样在 os.system() 中使用变量时,会遇到严重的安全风险和功能性错误。

错误且危险的方式
假设你想根据用户输入来删除一个文件:
import os
user_file = input("请输入要删除的文件名: ")
# 假设用户输入 "my_file.txt; rm -rf /"
# 这是一个恶意输入,叫做 "命令注入攻击"
# 错误的写法!
os.system(f"rm -rf {user_file}")
这里会发生什么?
-
命令注入攻击:如果用户输入
my_file.txt; rm -rf /,那么最终执行的命令会变成:rm -rf my_file.txt; rm -rf /
这会先删除
my_file.txt,然后极其危险地尝试删除你系统根目录下的所有文件!这绝对不是你想要的结果。
(图片来源网络,侵删) -
文件名包含空格:如果用户输入
my report.docx,命令会变成:rm -rf my report.docx
Shell 会把它解释成三个独立的参数:
rm,-rf,my,因为my不是一个有效的目录,所以命令会报错,文件report.docx不会被删除,这属于功能性错误。
根本原因:os.system() 会将你传入的字符串原封不动地交给操作系统的 Shell(如 /bin/sh 或 /bin/bash)去执行,Shell 会解析这个字符串中的空格、分号、管道符 、重定向符 > 等特殊字符,从而导致了上述问题。
安全且正确的方式:使用 subprocess 模块
从 Python 3.5 开始,官方文档就明确指出 os.system() 是“过时的”并且不推荐在新代码中使用,取而代之的是功能更强大、更安全的 subprocess 模块。

subprocess 模块允许你生成新的进程,连接它们的输入/输出/错误管道,并获得它们的返回码。
推荐方法 1:subprocess.run() (现代、推荐的方式)
这是目前最推荐的方法,它简单、直观且安全。
关键点:将命令和参数作为列表传递给 subprocess.run()。subprocess 会负责安全地拼接它们,而不会让 Shell 来解析,从而完全避免了命令注入的风险。
import subprocess
# 1. 定义命令和参数为列表的元素
command = "ls" # 命令本身
argument = "-l" # 参数
target_file = "important_file.txt" # 变量
# 2. 将所有元素组合成一个列表
# 这是安全的关键!
cmd_list = [command, argument, target_file]
# 3. 使用 subprocess.run() 执行
# shell=False 是默认且安全的设置
# check=True 会在命令返回非零退出码(即失败)时抛出异常
try:
print(f"准备执行命令: {' '.join(cmd_list)}")
result = subprocess.run(cmd_list, check=True, text=True, capture_output=True)
# 打印命令的标准输出
print("命令执行成功!")
print("标准输出:")
print(result.stdout)
except FileNotFoundError:
print(f"错误: 命令 '{command}' 未找到。")
except subprocess.CalledProcessError as e:
print(f"命令执行失败,返回码: {e.returncode}")
print("标准错误:")
print(e.stderr)
代码解析:
cmd_list = ["ls", "-l", "important_file.txt"]:我们将命令拆分成了一个列表。subprocess.run(cmd_list, ...):我们把这个列表传递给run函数。shell=False(默认值):告诉subprocess不要使用 Shell 来执行命令,而是直接运行ls程序,这阻止了 Shell 对任何特殊字符(如 , ,>)的解析。check=True:如果命令执行失败(文件不存在),subprocess会抛出CalledProcessError异常,你可以用try...except来捕获它。text=True:将stdout和stderr作为文本字符串返回,而不是字节。capture_output=True:捕获命令的输出和错误,存储在result.stdout和result.stderr中。
推荐方法 2:subprocess.Popen() (更灵活、底层的方式)
如果你需要更精细地控制进程的输入、输出和错误流,或者需要与进程进行实时交互,可以使用 Popen。
import subprocess
cmd_list = ["ping", "-c", "4", "google.com"] # ping 4次 google.com
# 使用 Popen 启动进程
process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# 等待进程结束,并获取输出
stdout, stderr = process.communicate()
print(f"进程返回码: {process.returncode}")
print("标准输出:")
print(stdout)
if stderr:
print("标准错误:")
print(stderr)
代码解析:
subprocess.Popen(...):启动一个子进程,但它不会等待进程结束。stdout=subprocess.PIPE:告诉Popen捕获标准输出流。stderr=subprocess.PIPE:告诉Popen捕获标准错误流。process.communicate():等待进程结束,并读取所有捕获的输出,这是一个阻塞调用。
| 特性 | os.system() |
subprocess.run() |
subprocess.Popen() |
|---|---|---|---|
| 安全性 | 低,有命令注入风险 | 高,通过列表传递参数,避免Shell解析 | 高,同 run |
| 易用性 | 简单,但功能有限 | 非常简单,是现代Python的推荐方式 | 灵活,但更复杂,需要更多代码 |
| 获取输出 | 困难,只能通过重定向到文件 | 非常简单,通过 result.stdout 获取 |
需要使用 communicate() 或轮询 stdout |
| 错误处理 | 返回退出码,需要手动检查 | 推荐 check=True,自动抛出异常 |
需要手动检查 process.returncode |
| 推荐度 | 不推荐 | 强烈推荐 | 在需要高级控制时使用 |
永远不要使用 os.system() 来处理用户输入或任何不可信的变量。
请始终使用 subprocess 模块,特别是 subprocess.run()。
核心原则是:将命令和它的参数作为列表传递给 subprocess 函数,而不是将整个命令拼接成一个字符串。 这样可以确保 subprocess 自己处理参数的拼接,从而彻底杜绝命令注入漏洞。
