杰瑞科技汇

Python os.system 如何正确使用变量?

为什么直接用变量会有风险?

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

Python os.system 如何正确使用变量?-图1
(图片来源网络,侵删)

错误且危险的方式

假设你想根据用户输入来删除一个文件:

import os
user_file = input("请输入要删除的文件名: ")
# 假设用户输入 "my_file.txt; rm -rf /" 
# 这是一个恶意输入,叫做 "命令注入攻击"
# 错误的写法!
os.system(f"rm -rf {user_file}")

这里会发生什么?

  1. 命令注入攻击:如果用户输入 my_file.txt; rm -rf /,那么最终执行的命令会变成:

    rm -rf my_file.txt; rm -rf /

    这会先删除 my_file.txt,然后极其危险地尝试删除你系统根目录下的所有文件!这绝对不是你想要的结果。

    Python os.system 如何正确使用变量?-图2
    (图片来源网络,侵删)
  2. 文件名包含空格:如果用户输入 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 模块。

Python os.system 如何正确使用变量?-图3
(图片来源网络,侵删)

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:将 stdoutstderr 作为文本字符串返回,而不是字节。
  • capture_output=True:捕获命令的输出和错误,存储在 result.stdoutresult.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 自己处理参数的拼接,从而彻底杜绝命令注入漏洞。

分享:
扫描分享到社交APP
上一篇
下一篇