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