Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 建议扩展 subprocessshlex 模块以原生支持 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 提出了对标准库的两个主要添加:

  1. shlex 模块中一个新的 sh() 渲染器函数,用于安全的 shell 命令构造
  2. subprocess 模块的核心函数添加 t-字符串支持,
    特别是 subprocess.Popensubprocess.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-字符串的自然扩展来教授,以展示其实用价值。

  1. 介绍命令注入问题以及为什么 f-字符串与 shell 命令一起使用是危险的。
  2. 展示传统解决方案(基于列表的命令、手动转义)。
  3. 介绍 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
    
  4. 介绍 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 的范围。


来源:https://github.com/python/peps/blob/main/peps/pep-0787.rst

最后修改时间:2025-04-27 15:17:24 GMT