PEP 787 – t-문자열을 사용한 더 안전한 subprocess 사용
- Author:
- Nick Humrich <nick at humrich.us>, Alyssa Coghlan <ncoghlan at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Deferred
- Type:
- Standards Track
- Requires:
- 750
- Created:
- 13-Apr-2025
- Python-Version:
- 3.15
- Post-History:
- 14-Apr-2025
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
PEP 750은 f-문자열을 일반화한 템플릿 문자열(t-문자열)을 도입하여, 다양한 맥락에서 문자열 보간을 안전하게 처리할 수 있는 방법을 제공했습니다. 이 PEP는 subprocess 모듈과 shlex 모듈이 t-문자열을 네이티브로 지원하도록 확장할 것을 제안합니다. 이를 통해 보간된 값이 들어간 셸 명령을 더 안전하고 편리하게 실행할 수 있으며, 이 확장은 API 사용성을 개선하는 t-문자열 기능의 참조 구현 역할도 합니다.
PEP 보류
PEP 초기 초안을 논의하는 과정에서, t-문자열을 사용하면 복잡한 subprocess 호출에서 shell=True 수준의 문법적 편의성을 제공할 수 있다는 점이 분명해졌습니다. 사용자가 제공한 텍스트에 시스템 셸 전체에 대한 접근을 허용할 때 생기는 보안 문제와 크로스 플랫폼 호환성 문제를 모두 피하면서도 그러한 편의성을 얻을 수 있습니다.
이를 위해 PEP 작성자들은 Python 3.15를 위한 제안의 개정 초안을 준비하기 전에 Python 3.14 베타 기간 동안(그리고 그 이후에도) 실험적인 t-문자열 기반 subprocess 호출 라이브러리를 개발할 계획입니다.
동기
템플릿 문자열은 PEP 750에서 안전성과 유연성을 제공하지만, 표준 라이브러리에는 템플릿 문자열의 실제 활용을 보여 주는 구체적인 소비자 구현이 없습니다. 철회된 PEP 501에서 언급했듯이, t-문자열의 가장 설득력 있는 사용 사례 중 하나는 더 안전한 셸 명령 실행입니다:
# 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-문자열 지원에 이상적인 후보인 이유는 여러 가지입니다:
- 셸 명령의 명령 주입 취약점은 잘 알려진 보안 위험입니다.
subprocess모듈은 이미 문자열 기반 명령 명세와 리스트 기반 명령 명세를 모두 지원합니다.- t-문자열과 올바른 셸 이스케이프 사이에는 자연스러운 매핑이 존재하며, 이는 편의성과 안전성을 모두 제공합니다.
- 개발자가 바로 이해하고 그 가치를 알아볼 수 있는 t-문자열의 실용적인 사례가 됩니다.
subprocess가 t-문자열을 네이티브로 처리하도록 확장하면, 많은 개발자가 안전하지 않을 수 있는 f-문자열을 사용하게 된 이유인 편의성을 희생하지 않고도 안전한 코드를 더 쉽게 작성할 수 있습니다.
명세
이 PEP는 표준 라이브러리에 두 가지 주요 추가 사항을 제안합니다:
- 안전한 셸 명령 구성을 위해
shlex모듈에 추가되는 새로운sh()렌더러 함수 subprocess모듈의 핵심 함수에 t-문자열 지원 추가,- 특히
subprocess.Popen,subprocess.run()및 명령 인자를 받는 기타 관련 함수
shlex에 추가되는 셸 이스케이프용 렌더러
참조 구현으로서 안전한 POSIX 셸 이스케이프를 위한 렌더러가 shlex 모듈에 추가됩니다. 이 렌더러의 이름은 sh이며, 템플릿 리터럴의 각 필드 값에 shlex.quote를 호출하는 것과 동일합니다.
따라서:
os.system(shlex.sh(t"cat {myfile}"))
다음과 동일하게 동작합니다:
os.system("cat " + shlex.quote(myfile)))
shlex.sh가 추가되더라도 shell=True를 전달하는 것은 피하는 것이 좋다는 subprocess 문서의 기존 경고는 바뀌지 않으며, 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-문자열을 명시적으로 이스케이프할 수 있습니다:
import shlex
# Safe POSIX-compliant shell command construction
command = shlex.sh(t"cat {filename}")
os.system(command)
subprocess 모듈 변경 사항
shlex 모듈에 렌더러가 추가되고 템플릿 문자열이 도입됨에 따라, subprocess 모듈이 템플릿 문자열을 Popen의 추가 입력 타입으로 받아 처리하도록 변경할 수 있습니다. 이미 시퀀스나 문자열을 받아 각각 다르게 동작하는 것과 마찬가지입니다. 그 결과, subprocess.run()과 같은 모든 subprocess.Popen 상위 수준 함수가 문자열을 안전하게 받을 수 있게 됩니다(shell=False의 경우 모든 시스템에서, shell=True의 경우 POSIX 시스템에서).
예를 들어:
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=True를 사용하는 경우, 안전성은 POSIX 호환 셸로 한정된다는 점에 유의해야 합니다. cmd.exe나 PowerShell이 셸로 사용될 수 있는 Windows 시스템에서는 shlex.quote()가 제공하는 이스케이프 메커니즘만으로는 모든 형태의 명령 주입을 막기에 충분하지 않습니다.
교육 방법
이 기능은 t-문자열의 실용적 가치를 보여 주는 자연스러운 확장으로 가르칠 수 있습니다:
- 명령 주입 문제와 f-문자열이 셸 명령에서 위험한 이유를 소개하십시오
- 전통적인 해결책(리스트 기반 명령, 수동 이스케이프)을 보여 줍니다.
- 명시적인 셸 이스케이프를 위한
shlex.sh렌더러를 소개합니다.:# 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 셸에 대한 이스케이프 렌더링 지원 보류
shlex.quote()는 정규식 문자 집합 [\w@%+=:,./-]를 안전한 것으로 분류하고 그 밖의 모든 문자는 안전하지 않은 것으로 간주하여, 그러한 문자를 포함하는 문자열은 따옴표로 감싸도록 하는 방식으로 동작합니다. 이때 사용되는 따옴표 처리 메커니즘은 POSIX 셸에서 문자열 따옴표 처리가 동작하는 방식에 특화되어 있으므로, POSIX 셸 문자열 따옴표 규칙을 따르지 않는 셸을 실행할 때는 신뢰할 수 없습니다.
예를 들어, POSIX 따옴표 규칙을 따르는 셸을 사용할 때 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의 범위를 벗어납니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.