PEP 787 – 使用 t-字符串更安全地使用子进程
- 作者:
- Nick Humrich <nick at humrich.us>, Alyssa Coghlan <ncoghlan at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 要求:
- 750
- 创建日期:
- 2025年4月13日
- Python 版本:
- 3.15
- 发布历史:
- 2025年4月14日
摘要
PEP 750 引入了模板字符串(t-字符串),作为 f-字符串的泛化,提供了一种在各种上下文中安全处理字符串插值的方法。本 PEP 建议扩展 subprocess
和 shlex
模块以原生支持 t-字符串,从而能够更安全、更符合人体工程学地执行带有插值值的 shell 命令,并作为 t-字符串功能的参考实现,以改善 API 的人体工程学。
PEP 延期
在 PEP 初稿的讨论中,很明显,t-字符串提供了一个潜在的机会,可以在不涉及用户提供的文本访问完整系统 shell 所带来的所有安全和跨平台兼容性问题的情况下,为复杂的子进程调用提供 shell=True
级别的语法便利。
为此,PEP 作者现在计划在 Python 3.14 beta 期间(及以后)开发一个实验性的基于 t-字符串的子进程调用库,然后为 Python 3.15 准备一份修订后的提案草案。
动机
尽管 PEP 750 中模板字符串提供了安全优势和灵活性,但它们在标准库中缺乏具体的消费者实现来展示其实际应用。t-字符串最引人注目的用例之一是更安全的 shell 命令执行,正如已撤回的 PEP 501 中所述。
# Unsafe with f-strings:
os.system(f"echo {message_from_user}")
# Also unsafe with f-strings
subprocess.run(f"echo {message_from_user}", shell=True)
# Fails with f-strings
subprocess.run(f"echo {message_from_user}")
# Safe with t-strings and POSIX-compliant shell quoting:
subprocess.run(t"echo {message_from_user}", shell=True)
# Safe on all platforms with t-strings:
subprocess.run(t"echo {message_from_user}")
# Safe on all platforms without t-strings:
subprocess.run(["echo", str(message_from_user)])
目前,开发人员必须在便利性(使用 f-字符串,可能存在安全风险)和安全性(使用更冗长的、基于列表的 API)之间做出选择。通过向 subprocess
模块添加原生 t-字符串支持,我们提供了一个消费者参考实现,展示了 t-字符串的价值,同时解决了常见的安全问题。
基本原理
subprocess 模块是 t-字符串支持的理想选择,原因如下:
- Shell 命令中的命令注入漏洞是众所周知的安全风险。
subprocess
模块已经支持基于字符串和基于列表的命令规范。- t-字符串和适当的 shell 转义之间存在自然映射,既提供了便利性又保证了安全性。
- 它作为 t-字符串的实用展示,开发人员可以立即理解和欣赏。
通过扩展 subprocess 以原生处理 t-字符串,我们使编写安全代码变得更容易,而无需牺牲导致许多开发人员使用可能不安全的 f-字符串的便利性。
规范
本 PEP 提出了对标准库的两个主要添加:
shlex
模块中一个新的sh()
渲染器函数,用于安全的 shell 命令构造- 向
subprocess
模块的核心函数添加 t-字符串支持, - 特别是
subprocess.Popen
、subprocess.run()
以及其他接受命令参数的相关函数。
- 向
用于 shell 转义的渲染器已添加到 shlex
作为参考实现,一个用于安全 POSIX shell 转义的渲染器将添加到 shlex
模块。此渲染器将命名为 sh
,并且等同于对模板字面量中的每个字段值调用 shlex.quote
。
因此
os.system(shlex.sh(t"cat {myfile}"))
将具有与以下相同的行为
os.system("cat " + shlex.quote(myfile)))
添加 shlex.sh
不会改变 subprocess
文档中关于应避免传递 shell=True
的现有告诫,也不会改变 os.system()
文档中对更高级别的 subprocess
API 的引用。
t-字符串处理器实现将如下所示:
from string.templatelib import Template, Interpolation
def sh(template: Template) -> str:
parts: list[str] = []
for item in template:
if isinstance(item, Interpolation):
# shlex.sh implementation, so shlex.quote can be used directly
parts.append(quote(str(item.value)))
else:
parts.append(item)
# shlex.sh implementation, so `join` references shlex.join
return join(parts)
这允许对 t-字符串进行显式转义以用于 shell。
import shlex
# Safe POSIX-compliant shell command construction
command = shlex.sh(t"cat {filename}")
os.system(command)
对 subprocess 模块的更改
在 shlex 模块中添加了额外的渲染器并添加了模板字符串后,subprocess
模块可以更改为处理接受模板字符串作为 Popen
的额外输入类型,因为它已经接受序列或字符串,并对每种类型具有不同的行为。作为回报,所有 subprocess.Popen
高级函数(例如 subprocess.run()
)都可以安全地接受字符串(对于 shell=False
在所有系统上,以及对于 POSIX 系统 对于 shell=True
)。
例如
subprocess.run(t"cat {myfile}", shell=True)
将自动使用本 PEP 中提供的 shlex.sh
渲染器。因此,在 subprocess.run
调用中像这样使用 shlex
subprocess.run(shlex.sh(t"cat {myfile}"), shell=True)
将是多余的,因为 run
将自动通过 shlex.sh
渲染任何模板字面量。
当调用 subprocess.Popen
而不带 shell=True
时,t-字符串支持仍将为 subprocess 提供更符合人体工程学的语法。例如
subprocess.run(t"cat {myfile} --flag {value}")
将等同于
subprocess.run(["cat", myfile, "--flag", value])
或者,更准确地说
subprocess.run(shlex.split(f"cat {shlex.quote(myfile)} --flag {shlex.quote(value)}"))
它将首先使用 shlex.sh
渲染器(如上),然后对结果使用 shlex.split
。
subprocess.Popen._execute_child
中的实现将检查 t-字符串。
from string.templatelib import Template
if isinstance(args, Template):
import shlex
if shell:
args = shlex.sh(args)
else:
args = shlex.split(shlex.sh(args))
向后兼容性
此更改完全向后兼容,因为它只添加了新功能,而未更改现有行为。subprocess 模块将继续以与当前相同的方式处理字符串和列表。
安全隐患
本 PEP 旨在通过提供一种更安全的替代方案来改进安全性,以替代将 f-字符串与 shell 命令一起使用。通过根据上下文(shell 或非 shell)自动应用适当的转义,它有助于防止命令注入漏洞。
但是,值得注意的是,当使用 shell=True
时,安全性仅限于符合 POSIX 的 shell。在 Windows 系统上,当 cmd.exe 或 PowerShell 可能用作 shell 时,shlex.quote()
提供的转义机制不足以防止所有形式的命令注入。
如何教授此内容
此功能可以作为 t-字符串的自然扩展来教授,以展示其实用价值。
- 介绍命令注入问题以及为什么 f-字符串与 shell 命令一起使用是危险的。
- 展示传统解决方案(基于列表的命令、手动转义)。
- 介绍
shlex.sh
渲染器,用于显式 shell 转义。# Unsafe: os.system(f"cat {filename}") # Potential command injection! # Safe using shlex.sh: os.system(shlex.sh(t"cat {filename}")) # Explicitly escaping for shell
- 介绍 subprocess 模块的原生 t-字符串支持。
# Unsafe: subprocess.run(f"cat {filename}", shell=True) # Potential command injection! # Safe but verbose: subprocess.run(["cat", filename]) # Safe and readable with t-strings: subprocess.run(t"cat {filename}", shell=True) # Automatically escapes filename subprocess.run(t"cat {filename}") # Automatically converts to list form
此实现应添加到 shlex 和 subprocess 模块文档中,并附有清晰的示例和安全建议。
推迟对非 POSIX shell 的转义渲染支持
shlex.quote()
的工作原理是将正则表达式字符集 [\w@%+=:,./-]
分类为安全字符,将所有其他字符视为不安全字符,因此需要引用包含它们的字符串。然后,使用的引用机制特定于 POSIX shell 中字符串引用的工作方式,因此在运行不遵循 POSIX shell 字符串引用规则的 shell 时不可信。
例如,在使用遵循 POSIX 引用规则的 shell 时,运行 subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True)
是安全的
$ cat > run_quoted.py
import sys, shlex, subprocess
subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True)
$ python3 run_quoted.py pwd
pwd
$ python3 run_quoted.py '; pwd'
; pwd
$ python3 run_quoted.py "'pwd'"
'pwd'
但在运行由 Python 调用的 cmd.exe
(或 Powershell)时仍然不安全
S:\> echo import sys, shlex, subprocess > run_quoted.py
S:\> echo subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True) >> run_quoted.py
S:\> type run_quoted.py
import sys, shlex, subprocess
subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True)
S:\> python3 run_quoted.py "echo OK"
'echo OK'
S:\> python3 run_quoted.py "'& echo Oh no!"
''"'"'
Oh no!'
解决此标准库限制超出了本 PEP 的范围。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0787.rst
最后修改时间:2025-04-27 15:17:24 GMT