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