PEP 578 – Python 런타임 감사 훅
- Author:
- Steve Dower <steve.dower at python.org>
- BDFL-Delegate:
- Christian Heimes <christian at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 16-Jun-2018
- Python-Version:
- 3.8
- Post-History:
- 28-Mar-2019, 07-May-2019
번역·라이선스 안내
이 문서는 Open Publication License v1.0 이상에 따라 만든 수정된 한국어 번역본입니다. 수정자: yeokja/yeokja 프로젝트. 수정일: 2026-08-29. 변경 내용: 영어 원문을 한국어로 번역했습니다. 원저자와 저작권 표시는 위 Author 필드와 아래 Copyright 절에 유지했으며, 이 번역은 원저자의 승인이나 보증을 뜻하지 않습니다. 수정되지 않은 기준 원문 · 공식 최신판 · Open Publication License v1.0
초록
이 PEP는 파이썬 런타임이 수행하는 동작을 감사 도구가 볼 수 있도록, 파이썬 API에 추가되는 사항과 CPython 구현의 구체적인 동작을 설명합니다. 이러한 동작을 볼 수 있게 되면 테스트 프레임워크, 로깅 프레임워크, 보안 도구가 런타임이 수행하는 동작을 모니터링하고 필요에 따라 제한할 수 있습니다.
이 PEP는 실행 중인 Python 애플리케이션에 대한 통찰을 제공하기 위해 두 가지 API를 추가할 것을 제안합니다. 하나는 임의의 이벤트를 위한 것이고, 다른 하나는 모듈 임포트 시스템에 특화된 것입니다. 이 API는 모든 Python 구현에서 사용할 수 있도록 하는 것을 목표로 하지만, 구현이 사용자에게 정보를 가장 잘 제공하는 방법을 자유롭게 결정할 수 있도록 여기서는 사용되는 구체적인 메시지와 값을 명시하지 않습니다. 설명을 위해 CPython에서 사용될 가능성이 높은 몇 가지 예를 제시합니다.
이러한 감사 API를 활용하여 Python 런타임의 보안을 강화하는 방안에 대한 논의와 권장 사항은 PEP 551을 참고하십시오.
배경
Python은 많은 일반적인 운영 체제에서 광범위한 저수준 기능에 대한 접근을 제공합니다. 이는 “한 번 작성하고, 어디서나 실행하는” 스크립팅에 매우 유용하지만, 동시에 Python으로 작성된 소프트웨어를 모니터링하기 어렵게 만들기도 합니다. Python은 네이티브 시스템 API를 직접 사용하기 때문에, 기존 모니터링 도구는 제한된 컨텍스트 문제나 감사 우회 문제를 겪습니다.
제한된 맥락은 시스템 모니터링이 어떤 동작이 일어났다는 사실은 보고할 수 있지만, 그 동작에 이르게 된 일련의 사건은 설명하지 못할 때 발생합니다. 예를 들어 OS 수준의 네트워크 모니터링은 “포트 5678에서 수신 대기 시작”을 보고할 수는 있어도, 해당 동작을 일으킨 시점의 프로세스 ID, 명령줄, 부모 프로세스, 프로그램의 로컬 상태는 제공하지 못할 수 있습니다. 이러한 동작을 막기 위한 방화벽 제어도 마찬가지로 제한적이어서 대개 프로세스 이름이나 현재 사용자와 같은 일부 전역 상태에 한정되며, 어떤 경우든 다른 애플리케이션 메시지와 연관 지을 수 있는 유용한 로그 파일을 제공하는 경우는 드뭅니다.
감사 우회는 어떤 작업에 사용되는 일반적인 시스템 도구라면 보통 그 사용을 보고하지만, Python을 통해 API에 접근하면 이것이 발생하지 않는 경우에 일어날 수 있습니다. 예를 들어, 감사되는 시스템에서는 HTTP 요청을 보내기 위해 “curl”을 호출하는 것이 특별히 모니터링될 수 있지만, Python의 “urlretrieve” 함수는 그렇지 않습니다.
오래 실행되는 Python 애플리케이션, 특히 웹 앱처럼 사용자가 제공한 정보를 처리하는 애플리케이션에서는 예기치 않은 동작이 발생할 위험이 있습니다. 이는 코드의 버그 때문일 수도 있고, 악의적인 사용자가 의도적으로 유발한 것일 수도 있습니다. 두 경우 모두 일반적인 애플리케이션 로깅이 우회되어, 비정상적인 일이 일어났다는 흔적이 전혀 남지 않을 수 있습니다.
또한, 다소 Python에 고유한 점으로, 임포트 시스템의 검색 경로를 조작하거나 의도한 것보다 경로상 앞쪽에 파일을 배치함으로써 애플리케이션에서 실행되는 코드에 영향을 주기가 매우 쉽습니다. 이는 개발자가 사용하려는 모듈과 같은 이름으로 스크립트를 만들 때 자주 볼 수 있습니다. 예를 들어 표준 라이브러리 random 모듈을 임포트하려는 random.py 파일이 그렇습니다.
이 제안은 악의적인 동작을 막으려는 것이 아니므로(그렇게 할 수 있는 새로운 방법을 몇 가지 제공하기는 하지만) 샌드박싱이 아닙니다. 더 자세한 논의는 아래의 Why Not A Sandbox 절을 참조하십시오.
변경 사항 개요
이러한 변경의 목적은 애플리케이션 개발자와 시스템 관리자 모두가 기존 모니터링 시스템의 모습이나 동작 방식을 강제하지 않으면서 Python을 그 시스템에 통합할 수 있도록 하는 것입니다.
이를 위해 두 가지 API 변경을 제안합니다. 바로 감사 훅(Audit Hook)과 검증된 열기 훅(Verified Open Hook)입니다. 두 가지 모두 Python과 네이티브 코드에서 사용할 수 있으므로, 순수 Python 코드로 작성된 애플리케이션과 프레임워크가 추가 메시지를 활용할 수 있으며, 동시에 임베더나 시스템 관리자가 감사가 항상 활성화된 Python 빌드를 배포할 수도 있습니다.
여기에서 설명하는 네이티브 API를 제공해야 하는 구현은 CPython뿐입니다. 다른 구현은 순수 Python API를 제공해야 하며, 기반 런타임에 적합하다면 네이티브 버전을 제공할 수도 있습니다. 감사 이벤트도 마찬가지로 구현에 따라 다른 것으로 간주되지만, 일반적인 기능 호환성 보장을 따릅니다.
감사 훅
런타임이 (호출자를 대신하여) 수행하는 동작을 관찰하려면 특정 작업 내부에서 메시지를 발생시키는 API가 필요합니다. 이러한 작업은 보통 Python 런타임이나 표준 라이브러리 깊숙한 곳에 있으며, 동적 코드 컴파일, 모듈 임포트, DNS 조회, 또는 ctypes와 같은 특정 모듈의 사용 등이 이에 해당합니다.
다음의 새로운 C API를 통해 임베더와 CPython 구현자는 감사 훅 메시지를 보내고 받을 수 있습니다:
# Add an auditing hook
typedef int (*hook_func)(const char *event, PyObject *args,
void *userData);
int PySys_AddAuditHook(hook_func hook, void *userData);
# Raise an event with all auditing hooks
int PySys_Audit(const char *event, PyObject *args);
감사 훅을 받고 발생시키기 위한 새로운 Python API는 다음과 같습니다:
# Add an auditing hook
sys.addaudithook(hook: Callable[[str, tuple]])
# Raise an event with all auditing hooks
sys.audit(str, *args)
훅은 Py_Initialize() 이전을 포함하여 언제든지 C에서 PySys_AddAuditHook()을 호출하거나, Python 코드에서 sys.addaudithook()을 호출하여 추가합니다. 훅은 제거하거나 교체할 수 없습니다. CPython의 경우 C에서 추가한 훅은 전역적인 반면, Python에서 추가한 훅은 현재 인터프리터에만 적용됩니다. 전역 훅은 인터프리터 훅보다 먼저 실행됩니다.
관심 있는 이벤트가 발생하면 코드는 C에서 (GIL을 보유한 상태로) PySys_Audit()을 호출하거나 sys.audit()을 호출할 수 있습니다. 문자열 인자는 이벤트의 이름이고, 튜플에는 인자가 담깁니다. 각 이벤트 이름은 인자에 대해 고정된 스키마를 가져야 하며, 이 스키마는 (각 x.y 버전 릴리스마다) 공개 API로 간주되어야 합니다. 따라서 스키마는 기능 릴리스 사이에서만, 문서를 갱신하면서 변경해야 합니다. 오버헤드를 최소화하고 네이티브 코드 훅 구현에서의 처리를 단순화하기 위해 이름 있는 인자는 지원하지 않습니다.
최대한의 호환성을 위해, 참조 인터프리터인 CPython의 이벤트와 같은 이름을 사용하는 이벤트는 호환되는 인자를 사용하도록 모든 노력을 기울여야 합니다. 구현별 이벤트 이름에 구현의 이름이나 약어를 포함하는 것도 충돌을 방지하는 데 도움이 됩니다. 예를 들어, pypy.jit_invoked 이벤트는 ipy.jit_invoked 이벤트와 명확하게 구별됩니다. Python 모듈에서 발생시키는 이벤트는 이벤트 이름에 해당 모듈이나 패키지의 이름을 포함해야 합니다.
이벤트 이름은 임의의 UTF-8 문자열일 수 있지만, 구현 간 일관성을 위해 유효한 Python 점 표기 이름을 사용하고 이름에 구체적인 세부 정보를 인코딩하지 않는 것이 권장됩니다. 예를 들어, 모듈 이름 spam을 인자로 받는 import 이벤트가 인자 없는 spam module imported 이벤트보다 바람직합니다. 이름 중간에 널 문자를 넣지 마십시오. 그렇지 않으면 C로 훅을 구현하는 사람들을 곤란하게 만들 수 있습니다.
이벤트가 감사될 때 각 훅은 (가능한 한) 추가된 순서대로 호출되며, 이벤트 이름과 인자가 전달됩니다. 어떤 훅이 예외가 설정된 상태로 반환되면 이후의 훅은 무시되며 일반적으로 Python 런타임은 종료되어야 합니다. 훅에서 발생한 예외는 처리되거나 예상된 상황으로 취급되도록 의도된 것이 아닙니다. 이를 통해 훅 구현은 특정 이벤트에 어떻게 대응할지 결정할 수 있습니다. 일반적인 대응은 이벤트를 기록하거나, 예외로 작업을 중단하거나, 운영 체제 종료 호출로 프로세스를 즉시 종료하는 것입니다.
이벤트가 감사되지만 설정된 훅이 없을 때 audit() 함수는 최소한의 오버헤드만 부과해야 합니다. 이상적으로는 각 인자가 감사 호출만을 위해 계산된 값이 아니라 기존 데이터에 대한 참조여야 합니다.
훅은 Python 객체일 수 있으므로 인터프리터 또는 런타임 종료 처리 중에 해제되어야 합니다. 이러한 해제는 다른 어떤 시점에도 일어나서는 안 되며, 예상치 못한 호출이 관찰될 수 있도록 이벤트 훅을 발생시켜야 합니다.
아래 Suggested Audit Hook Locations에서 감사 이벤트를 발생시켜야 하는 몇 가지 중요한 작업을 권장합니다. 일반적으로 이벤트는 가능한 한 가장 낮은 수준에서 발생시켜야 합니다. Python 코드와 네이티브 코드 중 어디에서 이벤트를 발생시킬지 선택할 수 있다면 네이티브 코드에서 발생시키는 것을 우선해야 합니다.
Python 구현은 어떤 작업이 감사 이벤트를 발생시키는지 이벤트 스키마와 함께 문서화해야 합니다. sys.addaudithook(print)가 모든 메시지를 표시하는 간단한 방법이 되도록 한 것은 의도된 것입니다.
검증된 열기 훅
대부분의 운영 체제에는 실행할 수 있는 파일과 실행할 수 없는 파일을 구별하는 메커니즘이 있습니다. 예를 들어 권한 필드의 실행 비트, 코드 변조 가능성을 탐지하기 위한 파일 내용의 검증된 해시, 파일 시스템 경로 제한 등이 이에 해당합니다. 이러한 메커니즘은 주어진 환경에서 승인된 코드만 실행되도록 보장하는 중요한 보안 메커니즘입니다.
대부분의 커널은 커널이 로드하고 실행하는 바이너리를 제한하거나 감사하는 방법을 제공합니다. Python이 소유한 파일 형식은 일반 데이터로 취급되므로 이러한 기능이 적용되지 않습니다. 이 열기 훅을 사용하면 Python 임베더가 스크립트를 실행하거나 Python 코드를 임포트할 때 운영 체제의 지원 기능과 통합할 수 있습니다.
검증된 열기 훅을 위한 새로운 공개 C API는 다음과 같습니다.:
# Set the handler
typedef PyObject *(*hook_func)(PyObject *path, void *userData)
int PyFile_SetOpenCodeHook(hook_func handler, void *userData)
# Open a file using the handler
PyObject *PyFile_OpenCode(const char *path)
검증된 열기 훅을 위한 새로운 공개 Python API는 다음과 같습니다.:
# Open a file using the handler
io.open_code(path : str) -> io.IOBase
io.open_code() 함수는 open(abspath(str(pathlike)), 'rb')를 그대로 대체할 수 있는 함수입니다. 기본 동작은 원시 바이너리 접근을 위해 파일을 여는 것입니다. 이 동작을 변경하려면 새 핸들러를 설정해야 합니다. 핸들러 함수는 str 인자만 받습니다. C API의 PyFile_OpenCode 함수는 UTF-8 인코딩을 가정합니다. 경로는 절대 경로여야 하며, 전체 경로가 올바르게 해석되도록 보장할 책임은 호출자에게 있습니다.
사용자 정의 핸들러는 Py_Initialize() 이전을 포함하여 언제든지 C에서 PyFile_SetOpenCodeHook()을 호출하여 설정할 수 있습니다. 그러나 훅이 이미 설정되어 있다면 이 호출은 실패합니다. 훅이 설정된 상태에서 open_code()가 호출되면, 경로가 훅에 전달되고 훅의 반환값이 그대로 반환됩니다. 반환되는 객체는 원시 바이트 읽기를 지원하는, 열린 파일류 객체여야 합니다. 이는 열기 핸들러가 이미 파일 전체를 메모리로 읽어 들인 경우 BytesIO 인스턴스를 허용하기 위해 명시적으로 의도된 것입니다.
이러한 훅은 CPython에서 자기 자신을 트리거하지 않고 _io.open() 함수를 임포트하여 호출할 수 있다는 점에 유의하십시오. 또한 _io.BytesIO를 사용하여 메모리 내 버퍼로 호환되는 결과를 반환할 수도 있습니다.
훅이 파일을 로드해서는 안 된다고 판단하면, 다른 로깅을 수행하는 것과 함께 원하는 예외를 발생시켜야 합니다.
파일에서 가져온 코드와 관련된 모든 임포트 및 실행 기능은 무조건 open_code()를 사용하도록 변경됩니다. compile(), exec(), eval() 호출은 이 함수를 거치지 않는다는 점에 유의해야 합니다. 이러한 호출의 코드를 포함하는 감사 훅이 파일에서 읽은 코드를 검증할 수 있는 가장 좋은 기회입니다. 현재 Python에서는 임포트와 실행이 분리되어 있으므로, 임포트되는 코드 대부분은 open_code()와 compile에 대한 로그 훅을 모두 거치게 됩니다. 따라서 검증 단계가 중복되지 않도록 주의해야 합니다.
의도적으로 코드를 실행하려는 것이 아닌 파일 접근에서는 이 함수를 사용하지 않을 것으로 예상됩니다. 여기에는 pickle, XML, YAML 파일을 로드하는 경우가 포함되며, 이러한 경우 코드 실행은 일반적으로 의도된 것이 아니라 악의적인 것으로 간주됩니다. 이러한 작업은 자체 감사 이벤트를 제공해야 하며, 가급적 일반 기능(예: Unpickler.load)과 코드 실행(Unpickler.find_class)을 구별하는 것이 좋습니다.
몇 가지 예를 들면, 파일 형식이 일반적으로 (POSIX에서) 실행 비트를 필요로 하거나 (Windows에서) 인터넷에서 다운로드한 것으로 표시되었을 때 경고를 띄운다면, 일반 open() 대신 open_code()를 사용하는 것이 좋습니다. ZipFile 클래스로 ZIP 파일을 열 때는 open()을 사용해야 하지만, zipimport를 통해 열 때는 올바른 의도를 나타내기 위해 open_code()를 사용해야 합니다. 특정 상황에 맞지 않는 함수를 사용하는 코드는 훅을 우회할 수 있으며, CPython과 표준 라이브러리에서는 이를 버그로 간주해야 합니다. 임의의 코드가 존재하는 상황에서 실행되는 모든 소스를 추적하려면 open_code 훅과 감사 훅을 조합하여 사용해야 합니다.
열기 훅을 변경하기 위한 Python API는 제공되지 않습니다. Python 코드에서 임포트 동작을 수정하려면 importlib이 제공하는 기존 기능을 사용하십시오.
API 가용성
여기에 추가된 모든 함수는 공개된 안정 API로 간주되지만, 함수의 동작은 구현마다 다릅니다. 여기에 있는 설명은 대부분 CPython 구현을 기준으로 합니다. 다른 구현도 이 함수들을 제공해야 하지만, 동일하게 동작해야 한다는 요구 사항은 없습니다.
예를 들어, sys.addaudithook()과 sys.audit()은 존재해야 하지만 아무 동작도 하지 않을 수 있습니다. 이렇게 하면 코드가 존재 여부를 검사하지 않고도 sys.audit()을 호출할 수 있지만, 그 호출이 어떤 효과를 가질 것이라고 가정해서는 안 됩니다. (보안이 중요한 코드에 존재 여부 검사를 포함하면 감사를 우회할 또 다른 경로가 생기므로, 함수가 항상 존재하는 편이 바람직합니다.)
io.open_code(path)는 최소한 항상 _io.open(path, 'rb')를 반환해야 합니다. 이 함수를 사용하는 코드는 무엇이 일어날 수 있는지에 대해 더 이상의 가정을 해서는 안 되며, CPython 이외의 구현은 개발자가 훅으로 이 함수의 동작을 재정의할 수 있도록 허용할 필요가 없습니다.
권장 감사 훅 위치
sys.audit() 또는 PySys_Audit() 호출의 위치와 매개변수는 개별 Python 구현이 결정합니다. 이는 각 구현이 자신의 플랫폼과 가장 관련 있는 작업을 노출하고, 비용이 크거나 지나치게 잦을 수 있는 이벤트를 피하거나 무시할 수 있도록 최대한의 자유를 주기 위함입니다.
표 1은 모든 구현에서 감사 이벤트를 발생시켜야 하는 작업에 대한 제안이자 이벤트 스키마의 예시 역할을 합니다.
표 2는 필수는 아니지만 CPython에서 사용할 수 있을 가능성이 높은 추가 예시를 제공합니다.
어떤 작업이 감사 이벤트를 제공하는지 확인하려면 사용 중인 Python 버전의 문서를 참조하십시오.
| API Function | Event Name | Arguments | Rationale |
|---|---|---|---|
PySys_AddAuditHook |
sys.addaudithook |
Detect when new audit hooks are being added. | |
PyFile_SetOpenCodeHook |
cpython.PyFile_SetOpenCodeHook |
Detects any attempt to set the open_code hook. |
|
compile, exec, eval, PyAst_CompileString,
PyAST_obj2mod |
compile |
(code, filename_or_none) |
Detect dynamic code compilation, where code could be a string or
AST. Note that this will be called for regular imports of source
code, including those that were opened with open_code. |
exec, eval, run_mod |
exec |
(code_object,) |
Detect dynamic execution of code objects. This only occurs for explicit calls, and is not raised for normal function invocation. |
import |
import |
(module, filename, sys.path,
sys.meta_path, sys.path_hooks) |
Detect when modules are
imported. This is raised before the module name is resolved to a
file. All arguments other than the module name may be None if
they are not used or available. |
open |
io.open |
(path, mode, flags) |
Detect when a
file is about to be opened. path and mode are the usual parameters
to open if available, while flags is provided instead of mode
in some cases. |
PyEval_SetProfile |
sys.setprofile |
Detect when code is
injecting trace functions. Because of the implementation, exceptions
raised from the hook will abort the operation, but will not be
raised in Python code. Note that threading.setprofile eventually
calls this function, so the event will be audited for each thread. |
|
PyEval_SetTrace |
sys.settrace |
Detect when code is
injecting trace functions. Because of the implementation, exceptions
raised from the hook will abort the operation, but will not be
raised in Python code. Note that threading.settrace eventually
calls this function, so the event will be audited for each thread. |
|
_PyObject_GenericSetAttr, check_set_special_type_attr,
object_set_class, func_set_code, func_set_[kw]defaults |
object.__setattr__ |
(object, attr, value) |
Detect monkey
patching of types and objects. This event
is raised for the __class__ attribute and any attribute on
type objects. |
_PyObject_GenericSetAttr |
object.__delattr__ |
(object,
attr) |
Detect deletion of object attributes. This event is raised
for any attribute on type objects. |
Unpickler.find_class |
pickle.find_class |
(module_name,
global_name) |
Detect imports and global name lookup when unpickling. |
| API Function | Event Name | Arguments | Rationale |
|---|---|---|---|
_PySys_ClearAuditHooks |
sys._clearaudithooks |
Notifies hooks they are being cleaned up, mainly in case the event is triggered unexpectedly. This event cannot be aborted. | |
code_new |
code.__new__ |
(bytecode, filename, name) |
Detect dynamic creation of code objects. This only occurs for direct instantiation, and is not raised for normal compilation. |
func_new_impl |
function.__new__ |
(code,) |
Detect dynamic creation of function objects. This only occurs for direct instantiation, and is not raised for normal compilation. |
_ctypes.dlopen, _ctypes.LoadLibrary |
ctypes.dlopen |
(module_or_path,) |
Detect when native modules are used. |
_ctypes._FuncPtr |
ctypes.dlsym |
(lib_object, name) |
Collect information about specific symbols retrieved from native modules. |
_ctypes._CData |
ctypes.cdata |
(ptr_as_int,) |
Detect
when code is accessing arbitrary memory using ctypes. |
new_mmap_object |
mmap.__new__ |
(fileno, map_size, access,
offset) |
Detects creation of mmap objects. On POSIX, access may
have been calculated from the prot and flags arguments. |
sys._getframe |
sys._getframe |
(frame_object,) |
Detect when code is accessing frames directly. |
sys._current_frames |
sys._current_frames |
Detect when code is accessing frames directly. | |
socket.bind, socket.connect, socket.connect_ex,
socket.getaddrinfo, socket.getnameinfo, socket.sendmsg,
socket.sendto |
socket.address |
(socket, address,) |
Detect access to network resources. The address is unmodified from the original call. |
member_get, func_get_code, func_get_[kw]defaults |
object.__getattr__ |
(object, attr) |
Detect access to restricted attributes. This event is raised for any built-in members that are marked as restricted, and members that may allow bypassing imports. |
urllib.urlopen |
urllib.Request |
(url, data, headers,
method) |
Detects URL requests. |
성능 영향
중요한 성능 영향은 이벤트가 발생하고 있지만 연결된 훅이 없는 경우입니다. 이는 피할 수 없는 경우입니다. 개발자가 감사 훅을 추가했다면 기능을 위해 성능을 희생하기로 명시적으로 선택한 것입니다. 훅이 추가된 경우의 성능 영향은 선택적으로 사용하는 기능이므로 여기서는 관심 대상이 아닙니다.
Python Performance Benchmark Suite [1]를 사용한 분석 결과 유의미한 영향은 없었으며, 대부분의 벤치마크 결과가 1.05배 빠른 것부터 1.05배 느린 것 사이에 있었습니다.
저희가 보기에 이 PEP에서 설명하는 감사 지점들이 성능에 미치는 영향은 무시할 수 있는 수준입니다.
기각된 아이디어
감사 훅을 위한 별도 모듈
감사 훅을 위한 새 모듈(가칭 audit)을 추가하자는 제안입니다. 이렇게 하면 API와 구현이 sys 모듈에서 분리되며, C 함수의 이름을 현재의 변형 대신 PyAudit_AddHook과 PyAudit_Audit으로 지을 수 있게 됩니다.
그러한 모듈은 항상 존재함이 보장되는 내장 모듈이어야 합니다. 이러한 훅은 그 특성상 아무 조건 없이 호출할 수 있는 호출 가능 객체여야 합니다. 조건부 임포트나 조건부 호출은 어떤 것이든 이벤트를 가로채서 억제하거나 수정할 기회를 주기 때문입니다.
가장 핵심적인 모듈 중 하나이므로 sys 모듈은 모듈 섀도잉 공격으로부터 어느 정도 보호됩니다. 애플리케이션이 계속 실행될 수 있을 만큼 충분히 기능하는 모듈로 sys를 대체하는 것은 관심 있는 함수 하나만 있는 모듈을 대체하는 것보다 훨씬 복잡한 작업입니다. sys 모듈을 섀도잉할 수 있는 공격자는 이미 파일에서 임의의 코드를 실행할 수 있는 반면, audit 모듈은 검색 경로상 어디에든 있는 .pth 파일의 한 줄만으로 대체될 수 있습니다.:
import sys; sys.modules['audit'] = type('audit', (object,),
{'audit': lambda *a: None, 'addhook': lambda *a: None})
sys나 audit에 대한 몽키 패칭 공격에는 이미 여러 겹의 보호 장치가 존재하지만, sys.modules에 대한 할당이나 삽입은 감사되지 않습니다.
이 아이디어는 audit 호출을 모두 억제하는 일을 너무 쉽게 만들기 때문에 거부되었습니다.
“감사” 모드를 나타내는 sys.flags의 플래그
이 제안은 Python이 “보안” 또는 “감사” 모드로 실행 중인지를 나타내는 값을 sys.flags에 추가하자는 것입니다. 이를 통해 애플리케이션은 일부 기능이 활성화되었거나 훅이 추가된 것을 감지하고 그에 맞게 동작을 수정할 수 있습니다.
현재로서는 감사 훅이 있을 때 프로그램이 다르게 동작해야 할 정당한 이유를 알지 못합니다.
애플리케이션 수준 API인 sys.audit과 io.open_code는 일반 python 진입점을 사용하든 다른 대체 진입점을 사용하든 관계없이 항상 존재하며 동작합니다. 호출자는 (부채널 분석을 수행하지 않는 한) 훅이 추가되었는지 여부를 판단할 수 없으며, 판단할 필요도 없습니다. 이 호출들은 호출자가 피할 필요가 없을 만큼 충분히 빨라야 하며, 추가된 훅이 애플리케이션 성능에 영향을 주지 않을 만큼 빠르도록 보장하는 것은 프로그램의 책임입니다.
이것이 “모호함을 통한 보안(security by obscurity)”이라는 인자(주장)는 타당하지만, 이 논의와는 무관합니다. 모호함을 통한 보안은 다른 보호 메커니즘이 없을 때에만 문제가 됩니다. 공격을 피하기 위한 첫 단계로서의 모호함은 강력히 권장됩니다(관련 논의는 이 글을 참조하십시오).
이러한 API의 사용 여부에 따라 애플리케이션이 동작을 바꿔야 할 적절한 이유가 없으므로 이 아이디어는 거부되었습니다.
샌드박스를 쓰지 않는 이유
CPython을 샌드박스화하려는 시도는 과거에 여러 번 있었으며, 지금까지의 시도는 모두 실패했습니다. 근본적인 문제는 샌드박스화된 코드를 실행할 때는 특정 기능을 제한해야 하지만, 그 외에는 Python이 정상적으로 동작하도록 해당 기능을 사용할 수 있어야 한다는 점입니다. 예를 들어 문자열을 바이트코드로 컴파일하는 기능을 완전히 제거하면 소스 코드에서 모듈을 임포트하는 기능도 망가지며, 완전히 제거하지 않으면 그 기능에 간접적으로 접근할 수 있는 방법이 너무 많습니다. 주어진 연산이 “안전”한지 아닌지를 일반적으로 판단할 수 있는 실현 가능한 방법은 아직 없습니다. 자세한 정보와 참고 자료는 [2]에서 확인할 수 있습니다.
이 제안은 기능을 제한하려는 것이 아니라, 단지 해당 기능이 사용되고 있다는 사실을 드러낼 뿐입니다. 특히 침입 시나리오에서는 탐지가 조기 차단보다 훨씬 더 중요합니다(조기 차단은 일반적으로 공격자가 탐지하기 더 어려운 다른 방법을 사용하도록 유도하기 때문입니다). 감사 훅을 사용할 수 있다는 것만으로는 Python의 공격 표면이 전혀 달라지지 않지만, 감사 훅은 방어자가 현재는 불가능한 방식으로 Python을 자신의 환경에 통합할 수 있게 해 줍니다.
감사 훅은 연산이 일어나는 것을 안전하게 막을 수 있으므로, 이 기능으로 어느 정도의 샌드박싱을 제공할 수 있는 것은 사실입니다. 그러나 대부분의 경우 의도는 샌드박스를 만드는 것이 아니라 로깅을 가능하게 하는 것입니다.
PEP 551과의 관계
이 API는 원래 PEP 551 Python 런타임의 보안 투명성(Security Transparency in the Python Runtime)의 일부로 제시되었습니다.
검토를 더 간단하게 하기 위해, 그리고 이 API들이 보안 이외의 영역에도 폭넓게 적용될 수 있기 때문에, 이제 API 설계를 별도로 제시합니다.
PEP 551은 Python을 보안 또는 감사 환경에 통합하는 방법을 다루는 정보성 PEP입니다.
참고 문헌
Copyright
Copyright (c) 2019 by Microsoft Corporation. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at https://spdx.org/licenses/OPUBL-1.0.html).