MicroPython과 WASM을 이용한 샌드박스 내 Python 코드 실행
요약
MicroPython과 WASM을 결합하여 안전한 Python 코드 실행 환경을 구축하는 방법을 소개합니다. 플러그인 시스템의 보안 취약점을 해결하기 위해 메모리, CPU, 파일 및 네트워크 접근을 엄격히 제한하는 샌드박스 구현을 목표로 합니다.
핵심 포인트
- MicroPython과 WASM을 활용한 샌드박스 환경 구축
- 플러그인 실행 시 발생할 수 있는 보안 및 시스템 리소스 위험 방지
- 메모리, CPU, 파일 시스템 및 네트워크 접근에 대한 엄격한 제어
- 호스트 함수와의 안전한 상호작용 지원
MicroPython과 WASM을 이용한 샌드박스 내 Python 코드 실행
2026년 6월 6일
저는 지난 몇 년 동안 코드를 샌드박스(sandbox)에서 실행하기 위한 다양한 접근 방식을 실험해 왔습니다. 하지만 최근의 시도는 제가 찾고 있었던 모든 특성을 마침내 갖추었을지도 모른다는 느낌이 듭니다. 저는 이를 micropython-wasm이라는 알파 패키지로 출시했으며, Datasette Agent를 위한 코드 실행 샌드박스 플러그인인 datasette-agent-micropython에서 이를 사용하고 있습니다.
왜 샌드박스가 필요한가?
저의 주요 오픈 소스 프로젝트인 Datasette, LLM, 심지어 sqlite-utils까지 모두 플러그인(plugin)을 지원합니다.
저는 소프트웨어를 확장하는 메커니즘으로서 플러그인을 매우 좋아합니다. 세심하게 설계된 플러그인 시스템은 새로운 시도를 할 때 발생하는 위험을 거의 제로에 가깝게 줄여줍니다. 아무리 기발한 아이디어라도 핵심 애플리케이션 자체에는 지속적인 영향을 미치지 않을 것입니다. 제 소프트웨어는 하룻밤 사이에 새로운 기능을 갖추며 성장할 수 있고, 저는 풀 리퀘스트(pull request)를 검토할 필요조차 없습니다!
하지만 한 가지 큰 단점이 있습니다. 저의 모든 플러그인 시스템은 Python과 Pluggy를 사용하며, 플러그인 코드는 제 애플리케이션 내에서 모든 권한을 가진 채 실행됩니다. 버그가 있거나 악의적인 플러그인은 모든 것을 망가뜨리거나 개인 데이터를 유출할 수 있습니다.
저는 플러그인 방식의 코드를 승인되지 않은 파일을 읽거나, 네트워크에 연결하거나, 일반적으로 애플리케이션의 나머지 부분이나 사용자의 컴퓨터에 위험하거나 해로운 방식으로 작동할 수 없는 환경에서 실행할 수 있기를 바랍니다.
저의 관심사는 단순히 플러그인에만 국한되지 않습니다. 특히 Datasette의 경우, 임의의 코드 실행(arbitrary code execution)이 유용할 것으로 보이는 많은 기능이 있습니다. 저는 이미 테이블에 저장된 값을 변환하는 데 코드를 사용할 수 있는 Datasette Enrichments를 위해 이를 실험해 보았습니다. 저는 승인된 위치에서 JSON을 가져오고, 아주 작은 코드를 실행하여 이를 딕셔너리(dictionary) 리스트로 재형식화한 다음, 이를 SQLite 데이터베이스 테이블의 행으로 삽입하는 작업을 정해진 일정에 따라 실행할 수 있는 메커니즘을 구축하고 싶습니다.
샌드박스에 기대하는 점
저의 목표는 제가 만든 Python 애플리케이션 내에서 코드를 안전하게 실행하는 것입니다. 제가 필요로 하는 사항은 다음과 같습니다:
- PyPI로부터 깔끔하게 설치되는 종속성: 필요한 경우 여러 플랫폼에 걸친 바이너리 휠 (binary wheels)을 포함해야 합니다. 제 소프트웨어를 사용하는 사람들이 제 Python 패키지를 직접 설치하는 것 외에 추가적인 단계를 거치지 않기를 원합니다.
- 실행되는 코드는 메모리 (memory) 및 CPU 제한을 모두 받아야 합니다.
while True: s += "longer string"과 같은 코드가 제 애플리케이션이나 사용자의 컴퓨터를 다운시키지 않기를 원합니다. - 파일 접근 (File access)은 엄격하게 제어되어야 합니다. 파일 시스템 접근을 아예 차단하거나, 어떤 파일을 읽을 수 있고 어떤 파일을 쓸 수 있는지 제가 정확하게 정의할 수 있어야 합니다.
- 네트워크 접근 (Network access) 또한 제어되어야 합니다. 샌드박스 내의 코드는 제가 완전히 제어하는 레이어를 거치지 않고는 그 무엇과도 통신할 수 없어야 합니다.
- 호스트 함수 (host functions)와의 상호작용 지원이 필요합니다. 실행 중인 코드에 선택된 플랫폼 기능들을 신중하게 노출할 수 없다면 샌드박스는 큰 의미가 없습니다.
- 견고하고, 지원되며, 명확하게 문서화되어야 합니다. 활발하게 유지 관리되지 않는다는 경고가 붙은 수많은 샌드박스 프로젝트들을 보아왔습니다!
WebAssembly가 여기서 매우 유망해 보입니다
웹 브라우저는 악성 코드와 관련하여 상상할 수 있는 가장 적대적인 환경에서 작동합니다. 브라우저의 역할은 거의 모든 페이지 로드 시 웹으로부터 신뢰할 수 없는 코드를 다운로드하고 실행하는 것입니다.
이를 고려할 때, JavaScript 엔진은 샌드박스의 훌륭한 후보가 될 수 있습니다. 안타깝게도 이러한 엔진들은 매우 복잡하며, 다른 프로젝트에 쉽게 임베딩(embedding)되도록 설계되지 않았습니다. 제가 본 대부분의 v8-in-Python 프로젝트들은 유지 관리가 드물게 이루어지며, 완전히 신뢰할 수 없는 코드와 함께 사용하지 말라는 경고가 포함되어 있습니다.
WebAssembly는 훨씬 더 나은 후보입니다. WebAssembly는 처음부터 제가 중요하게 생각하는 모든 특성을 지원하도록 설계되었으며, 거의 10년 동안 브라우저에서 테스트되어 왔습니다. wasmtime Python 라이브러리는 활발하게 유지 관리되고 있으며 바이너리 휠 (binary wheels)을 제공합니다.
WebAssembly에서의 MicroPython
wasmtime와 같은 WebAssembly 엔진은 WebAssembly 바이너리를 실행합니다. Rust와 같은 일부 프로그래밍 언어는 WebAssembly로 직접 컴파일하기 쉽습니다. JavaScript나 Python과 같은 동적 언어(Dynamic languages)는 eval()과 같은 언어 기본 기능(language primitives)을 지원하기 때문에 더 어렵습니다. 이는 런타임(runtime)에 완전한 인터프리터(interpreter)가 가용해야 함을 의미합니다.
Python을 실행하려면 WebAssembly로 컴파일된 완전한 Python 인터프리터가 필요하며, 코드를 쉽게 입력하고, 호스트 함수(host functions)를 연결하며, 결과에 접근할 수 있는 방식으로 구성되어야 합니다.
Pyodide는 브라우저에서 WebAssembly를 사용하여 Python을 실행하기 위한 뛰어난 패키지를 제공하지만, 서버 측 Python에서 Pyodide를 사용하는 것은 지원되지 않습니다. 제가 찾을 수 있었던 가장 최근의 조언은 2024년 10월의 내용으로, "Pyodide는 Emscripten 툴체인(toolchain)으로 구축되었으며 브라우저 또는 Node.js에서만 실행할 수 있습니다"라고 명시되어 있었습니다.
며칠 전, 저는 이를 위한 대안으로 MicroPython을 살펴보기로 했습니다. MicroPython 사이트에는 다음과 같이 적혀 있습니다:
MicroPython은 Python 3 프로그래밍 언어의 가볍고 효율적인 구현체로, Python 표준 라이브러리의 작은 부분 집합을 포함하며 마이크로컨트롤러(microcontrollers) 및 제약된 환경(constrained environments)에서 실행되도록 최적화되어 있습니다.
WebAssembly는 저에게 분명 제약된 환경처럼 느껴집니다!
첫 번째 버전 구축하기
저는 GPT-5.5 Pro에게 조사를 요청했고, 그 결과 Yamamoto Takahashi가 MicroPython에 대해 제출한 "Experimental WASI support for ports/unix"라는 제목의 PR(Pull Request)을 찾아냈습니다.
그 후 GPT-5.5 Pro는 이 research.md 문서를 생성했습니다. 그래서 저는 Codex Desktop과 GPT-5.5 Pro를 자유롭게 풀어놓고 어떤 결과가 나오는지 지켜보았습니다:
research.md 문서를 읽고 이것을 구축하세요. 이 프로젝트의 일부로 MicroPython의 커스텀 WASM 버전을 컴파일하는 스크립트를 작성해야 할 것입니다. 이를 위해 스크립트의 일부로서 MicroPython 코드를 /tmp 디렉토리로 가져오세요.
성공했습니다. 이제 저는 WebAssembly 샌드박스(sandbox) 내부에서 Python 코드를 실행할 수 있는 프로토타입 Python 라이브러리를 갖게 되었습니다!
해결하기 가장 까다로웠던 부분은 지속적인 인터프리터 상태(persistent interpreter state)였습니다. 여기서 사용 중인 WASM 빌드는 인터프리터를 시작하고, 코드를 실행한 다음, 마지막에 인터프리터를 종료하는 단일 진입점(entry point)만을 노출합니다.
이는 일회성 스크립트에는 잘 작동하지만, Datasette Agent의 경우 여러 번의 코드 실행 호출에 걸쳐 변수와 함수가 메모리에 상주(resident)하여 재사용할 수 있기를 원했습니다.
코딩 에이전트(coding agents)와 작업할 때의 멋진 점은 아이디어에서 개념 증명(proof of concept)까지 빠르게 도달할 수 있다는 것입니다. 저는 다음과 같이 프롬프트를 작성했습니다:
변수를 상주시키기 위해서: 만약 우리가 MicroPython 자체 내부에서 코드를 실행하면서, 호스트 함수인 get_next_python_code()를 호출하고 이를 eval()에 전달한다면 어떨까? 그리고 그 호스트 함수는 아마도 큐(queue)를 가진 스레드에서 실행됨으로써 새로운 코드가 사용 가능할 때까지 차단(block)되는 방식이라면? 이 방식이나 유사한 아이디어가 여기서 도움이 될 수 있을까?
몇 번의 반복 과정을 거쳐 우리는 실제로 작동하는 버전을 만들어냈습니다! 이제 Python 코드에서 다음과 같이 할 수 있습니다:
from micropython_wasm import MicroPythonSession
with MicroPythonSession() as session:
print(session.run("x = 10\nprint(x)").stdout)
print(session.run("x += 5\nprint(x)").stdout)
print(session.run("print(x * 2)").stdout)
내부적으로 이것은 스레드를 시작하고, 요청 큐(request queue)를 설정한 다음, session.run() 명령을 위해 해당 큐로 메시지를 보냅니다. 그리고 매번 실행 결과에 대한 응답 큐(reply queue)를 기다립니다. WASM 내부에서 MicroPython 인터프리터는 __session_next__() 호스트 함수가 다음 코드 라인을 반환할 때까지 차단(block)되어 대기하며, 해당 코드를 eval()로 실행한 후, 각 블록이 성공적으로 실행되면 __session_result__({"id": request_id, "ok": True})를 호출합니다.
또 다른 복잡한 부분은 호스트 함수(host functions)를 지원하는 것이었습니다. 이를 통해 제 Python 라이브러리가 MicroPython에서 실행되는 코드에 의해 호출될 수 있는 함수들을 선택적으로 노출할 수 있어야 했습니다.
Codex는 이를 78줄의 C 코드로 해결했으며, 이는 결과적으로 제가 패키지와 함께 배포하는 362KB 크기의 WebAssembly 블롭(blob)으로 컴파일되었습니다.
저는 결코 C 프로그래머가 아니지만, C 코드를 읽어보았고 두 개의 서로 다른 모델에게 설명을 요청했습니다 (여기 Claude의 설명이 있습니다). 그리고 수많은 테스트를 거쳤습니다.
WebAssembly (WASM)를 사용하여 작업할 때의 큰 장점은, 만약 C 코드가 치명적인 결함이 있는 것으로 드러나더라도 발생할 수 있는 최악의 상황은 WebAssembly 실행이 예외 (exception)와 함께 실패하는 것뿐이라는 점입니다. 저는 그 정도의 리스크는 감수할 수 있습니다.
메모리 제한 (Memory limits)은 wasmtime에 의해 직접적으로 지원됩니다. CPU 제한은 조금 더 어렵습니다. wasmtime은 WebAssembly 호출이 실행할 수 있는 연산 횟수를 제한하기 위해 "연료 (fuel)" 개념을 제공하며, 이는 이 문제에 적합한 방식이지만 그 단위(units)를 파악하기가 어렵습니다. 현재 기본 "연료" 설정을 2,000만으로 두고 실험 중이지만, 이것이 가장 적절한 값인지에 대해서는 확신이 없습니다.
제가 '바이브 코딩(vibe-coded)'한 샌드박스를 신뢰해도 될까요?
미성숙하고 관리가 소홀한 샌드박싱 라이브러리들에 대해 불평해 왔는데, 이제 제가 직접 만들었다는 사실이 매우 아이러니합니다!
저는 의도적으로 이 프로젝트에 알파(alpha) 릴리스 버전을 붙였으며, 상당한 리스크를 감수할 의사가 없는 분들에게는 아직 추천할 준비가 되지 않았습니다.
저는 스스로 사용해도 괜찮을 정도로 충분한 테스트를 거쳤습니다. 이를 사용하는 저의 첫 번째 플러그인인 datasette-agent-micropython을 출시했습니다. 또한 해당 Datasette Agent 플러그인 안에 GPT-5.5 xhigh를 가두고 샌드박스를 탈출하도록 도전하게 했는데, 지금까지는 성공하지 못했습니다.
저는 이 구현체가 전문적인 보안 팀과 중대한 문제를 안고 있는 일부 기업들이 샌드박싱 접근 방식으로서 WebAssembly 내의 Python 사용을 확정하고, 그들만의 솔루션을 오픈 소스로 공개하도록 설득할 수 있기를 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 RSS: Simon Willison's Weblog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기