bigywater.tech
Python · 69

subprocess:Python 如何启动并管理外部进程

真实构造一段包含shell特殊字符的字符串,对比shell=True字符串拼接和shell=False列表参数两种调用方式,验证命令注入真实发生的条件和防御方法,并确认timeout参数真的会主动终止子进程。

shell=True+字符串拼接
真实发生命令注入,分号后的内容被独立执行
shell=False+列表参数
真实同一段字符串被完整当成一个文件名
timeout=0.2对sleep 5
真实0.21秒就被主动终止

做的事:真实对比shell=True字符串拼接和shell=False列表参数

代码filename = "harmless.txt; echo INJECTED" # 模拟一个"不可信"的输入 r1 = subprocess.run(f"ls {filename}", shell=True, capture_output=True, text=True) r2 = subprocess.run(["ls", filename], shell=False, capture_output=True, text=True) print(r1.stdout) # shell=True,字符串拼接 print(r2.stderr) # shell=False,列表参数

验证:shell=True配合字符串拼接,分号后面的内容真实被当成了独立命令执行

实测 · 本机执行输出
shell=True with unsafe string interpolation, stdout:  'INJECTED\n'    ← 真实执行了echo INJECTED
stderr: 'ls: harmless.txt: No such file or directory\n'
这是shell=True真实的风险来源:subprocess.run(f"ls {filename}", shell=True, ...)真实的做法是把整个字符串"ls harmless.txt; echo INJECTED"原样交给一个真实的shell(/bin/sh)去解释执行,shell看到这个字符串里的;会真实把它当成命令分隔符,理解成"先跑ls harmless.txt,再跑echo INJECTED"两条独立的命令——本文这次stdout里真实出现了INJECTED,正是第二条命令真的被执行了的证据。这份filename如果真实来自用户输入、请求参数或者任何不受信任的数据源,攻击者能真实构造出这类分号+任意命令的输入,让服务端进程执行完全不受控的额外命令,这正是命令注入这类漏洞真实的典型成因。

验证:shell=False配合列表参数,同样的字符串真实完全没有被注入

实测 · 本机执行输出
shell=False with list args (same string, whole thing treated as one filename):
stdout: ''
stderr: 'ls: harmless.txt; echo INJECTED: No such file or directory\n'   ← 真实把整段字符串当成了一个文件名

这组对比给出了真实的防御方法:subprocess.run(["ls", filename], shell=False, ...)不经过任何shell解释,filename这个字符串真实作为一个不可分割的整体,直接被当成ls命令的一个参数(对应操作系统exec系列调用真实接收的参数数组)——即使它里面真实包含;这个在shell语法里有特殊含义的字符,ls看到的也只是"一个文件名恰好包含分号和空格",报错信息里stderr真实原样带上了整段字符串,证明它被当作了一个整体,没有任何部分被单独解释执行。真实的教训很直接:只要命令的某个参数来自任何不完全可信的数据源,就应该用shell=False配合列表参数(这也是subprocess模块的默认值),完全避免拼接字符串再交给shell解析这条路;如果确实需要shell的管道、通配符这类功能,也应该真实对每个参数单独用shlex.quote()转义,而不是自己手写字符串拼接。

验证:timeout参数真实会杀掉子进程,不是消极地等它自然结束

实测 · 本机执行输出
subprocess.run(["sleep", "5"], timeout=0.2)
→ real TimeoutExpired after 0.21 s: Command '['sleep', '5']' timed out after 0.2 seconds

真实测出的0.21秒跟设置的timeout=0.2几乎完全对应,证明subprocess.run的timeout参数不是简单地"等这么久再判断",而是真实在超时那一刻主动向子进程发送终止信号并抛出TimeoutExpired异常——本来要跑满5秒的sleep进程,真实在0.2秒左右就被提前杀掉了,不会真的让它继续占用系统资源跑到自然结束。这跟本系列前面验证过的asyncio的wait_for是同一类真实思路:给外部进程调用设一个超时上限,是避免某次真实调用意外挂起(网络请求卡住、外部命令死循环)拖垮整个程序的基本防御手段。

分步演示:从真实的命令注入到列表参数的完全免疫

每一步都是本文上面用真实的subprocess模块跑出来的原始结果。

同一段不可信字符串,shell=True和shell=False的真实结局完全不同
这一步新验证的调用方式

    本文用到的工具

    • 本机 Python 3.14.7,标准库 subprocess.run
    京ICP备2026067502号-1京公网安备11010502063487号