跳到主要内容

Python 子进程安全实践

对于启动、执行并返回有限结果的命令,首选 subprocess.run

import subprocess

completed = subprocess.run(
["git", "status", "--short"],
check=True,
capture_output=True,
text=True,
encoding="utf-8",
timeout=30,
)
print(completed.stdout)

关键原则:将可执行文件和每个参数作为列表中的独立元素传入。在不启用 Shell 的情况下,参数中的空格或 Shell 元字符(如 *, |)会被视为普通字符,但仍需检查目标程序对参数的解释。

结果处理与失败机制

  • 异常抛出:设置 check=True 后,若进程退出码非零,将抛出 CalledProcessError
  • 输出捕获capture_output=True 会同时捕获 stdout 和 stderr。若未开启 text=True,返回的是字节流(bytes)。
  • 超时控制timeout 限制的是进程创建后的通信时间。超时发生时,run() 会终止子进程并等待其退出,随后抛出 TimeoutExpired。注意:某些平台上的进程创建延迟可能无法被中断。
  • 内存安全:捕获无界输出可能导致内存耗尽。若输出量不可控,建议继承标准流、重定向到文件,或采用增量读取方式。

最佳实践:对于预期可能返回非零状态的场景,应显式检查 returncode,而不是简单设置 check=False 后忽略错误。

环境变量与工作目录

import os
from pathlib import Path

child_env = os.environ.copy()
child_env["APP_MODE"] = "batch"

subprocess.run(
["tool", "--input", "data.json"],
cwd=Path("workspace"),
env=child_env,
check=True,
)

注意env 参数会完全替换子进程的环境变量,而非追加。

  • 若需继承父进程环境,务必先复制 os.environ
  • 使用受控的最小化环境可以减少隐藏依赖,避免意外携带敏感凭据,并防止环境变量干扰可执行文件或库的查找路径。

在特权或安全敏感代码中,应显式指定可执行文件的完整路径,切勿假设调用方的 PATH 是可信的。

Shell 边界

仅当确实需要 Shell 特性(如管道、通配符展开)时才使用 shell=True。不要将其作为构建参数字符串的捷径。

  • 风险:启用 Shell 后,引号、变量展开、管道及重定向均成为执行逻辑的一部分,极易引发命令注入。
  • 禁忌:严禁将不可信的外部输入直接拼接进 Shell 命令字符串。
  • 平台差异:不同平台的 Shell 行为差异巨大,尤其是 Windows 批处理文件,需格外小心。

长驻与交互式进程

对于长期运行或交互式子进程,使用 Popen 并自行管理完整生命周期,包括:

  • 管道数据的排空(drainage)
  • 优雅关闭与信号处理
  • 超时监控
  • 资源清理

若需异步编排,使用 asyncio.create_subprocess_exec

死锁警告:若子进程可能填满未读取的管道缓冲区,直接调用 wait() 会导致死锁。应使用 communicate() 来协调常见的管道读写场景。

参数列表不等于输入校验

不经过 Shell 可以避免 Shell 展开,但子程序仍会解释参数。以 - 开头的文件名可能被当作选项;只有目标工具明确支持时,才能用 -- 分隔选项和文件名。Windows 批处理文件即使在 shell=False 时也可能由 Shell 执行,参见 subprocess 安全说明

程序未能启动(例如可执行文件不存在)会抛出 OSError,常见的是 FileNotFoundError,而不是 CalledProcessErrorrun() 超时后的清理针对直接子进程,不保证终止其所有后代。Popen.communicate(timeout=...) 超时不会自动杀死子进程;需要杀死它,再次调用 communicate(),排空管道并回收进程。

参考来源

探索关联

这篇笔记还没有文档关联。

同主题的其他笔记 (49)

打开关联网络