真实构造一段包含shell特殊字符的字符串,对比shell=True字符串拼接和shell=False列表参数两种调用方式,验证命令注入真实发生的条件和防御方法,并确认timeout参数真的会主动终止子进程。
代码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 with unsafe string interpolation, stdout: 'INJECTED\n' ← 真实执行了echo INJECTED stderr: 'ls: harmless.txt: No such file or directory\n'
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()转义,而不是自己手写字符串拼接。
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模块跑出来的原始结果。
subprocess.run