
“일단 돌려보고 문제 있으면 고치죠.” AI 코딩 도구가 뽑아준 스크립트를 터미널에 바로 붙여넣고 엔터를 눌렀다가 등골이 서늘해진 경험, 한 번쯤 있으실 거예요. LLM이 생성한 코드를 검토 없이 곧바로 실행하면 안 되는 이유는 명확합니다. 모델은 코드가 ‘그럴듯하게’ 보이도록 만드는 데는 뛰어나지만, 그 코드가 실제로 여러분의 파일 시스템과 네트워크에서 안전하게 동작할지는 보장하지 않기 때문이에요. 그래서 실무에서는 코드를 실행하는 환경 자체를 격리하는 샌드박싱을 기본값으로 두고 작업합니다.
검수 없이 실행하면 위험한 진짜 이유
LLM은 통계적으로 가장 그럴듯한 다음 토큰을 이어 붙일 뿐, 그 코드가 여러분의 시스템에서 어떤 부작용을 일으킬지 실행 전에 검증하는 과정을 거치지 않아요. 그래서 파일을 삭제하는 명령, 시스템 설정을 바꾸는 명령, 외부 서버로 데이터를 보내는 코드가 아무렇지 않게 섞여 나올 수 있습니다. 특히 프롬프트 인젝션 공격을 받으면 사용자가 요청하지도 않은 코드가 응답에 끼어들어 실행될 위험도 생겨요.
이런 문제는 보안 분야에서 이미 오래전부터 다뤄온 취약점 범주와 정확히 겹칩니다. 신뢰할 수 없는 입력이 코드로 해석되어 실행되는 문제는 코드 인젝션이라는 이름으로 분류되어 있고, 관련 정의는 MITRE가 관리하는 코드 인젝션 항목에서 확인할 수 있어요. LLM 출력을 ‘신뢰할 수 없는 입력’으로 취급하고 다뤄야 한다는 점이 핵심입니다.
존재하지도 않는 패키지를 설치하게 되는 상황
LLM이 생성한 코드를 실행하면 발생하는 또 다른 위험은 ‘패키지 할루시네이션’이에요. 모델이 실제로는 존재하지 않는 라이브러리 이름을 import 문에 적어 넣는 경우가 종종 있는데, 문제는 공격자가 그 존재하지 않는 이름을 미리 선점해 악성 패키지로 등록해두는 사례가 실제로 보고되고 있다는 점입니다. 이런 방식을 업계에서는 슬롭스쿼팅(slopsquatting)이라고 부르기 시작했어요.
이 경우 코드 자체는 문법적으로 멀쩡하고 실행 로그도 깔끔하게 남기 때문에, 설치 단계에서 걸러내지 않으면 검토자가 눈으로 봐도 알아채기 어렵습니다. pip install 명령을 코드와 함께 자동 실행하는 파이프라인을 쓰고 있다면, 패키지 설치 자체도 격리된 환경 안에서 네트워크를 제한한 채 이뤄지게 만들어야 이런 공급망 공격을 막을 수 있어요.

실무에서 쓰는 격리 수준 정리
샌드박싱이라고 다 같은 수준의 격리를 제공하지는 않아요. 격리 강도와 성능, 구축 난이도가 서로 다르기 때문에 상황에 맞는 방식을 골라야 합니다. 컴퓨터 보안에서 말하는 샌드박스라는 개념 자체는 샌드박스 문서에 정리되어 있는데, 실무에서는 아래처럼 구체적인 기술로 구현합니다.
| 방식 | 대표 기술 | 격리 수준 | 특징 |
|---|---|---|---|
| OS 권한 제한 | seccomp, AppArmor, chroot | 낮음~중간 | 커널을 공유해 오버헤드는 적지만 커널 취약점에는 그대로 노출됨 |
| 컨테이너 | Docker, Podman | 중간 | 설정만으로 네트워크·파일시스템·자원을 제한할 수 있어 가장 널리 씀 |
| 마이크로VM | Firecracker, gVisor | 높음 | 커널 자체를 가상화하거나 유저스페이스에서 시스템콜을 가로채 격리 강도를 높임 |
| WebAssembly | Wasmtime, Pyodide | 높음 | 능력 기반 보안 모델로 애초에 파일·네트워크 접근 권한이 기본 차단 상태 |
OpenAI의 코드 인터프리터(현재는 데이터 분석 기능)도 인터넷 연결이 끊긴 컨테이너 안에서 파이썬을 실행하는 방식이고, E2B 같은 서비스는 이런 격리 환경을 API로 제공해 AI 에이전트가 코드를 실행할 때 쓰도록 만들어졌어요. 개인 프로젝트라면 컨테이너 수준으로도 충분하고, 여러 사용자의 요청을 동시에 실행해야 하는 서비스라면 마이크로VM 수준까지 고려하는 게 안전합니다.
Docker로 최소 권한 실행 환경 만들어보기
가장 접근하기 쉬운 방법은 Docker로 권한을 최소화한 컨테이너를 만들어 그 안에서만 코드를 돌리는 거예요. 아래는 네트워크를 완전히 차단하고, 파일 시스템을 읽기 전용으로 만들고, 메모리와 프로세스 개수까지 제한하는 실행 명령입니다.
docker run --rm \
--network none \
--read-only \
--memory=256m \
--cpus=0.5 \
--pids-limit=64 \
--cap-drop=ALL \
--security-opt=no-new-privileges \
-v "$(pwd)/workspace:/workspace:ro" \
python:3.12-slim \
python /workspace/generated_code.py
이 옵션들은 모두 Docker 공식 CLI 레퍼런스에 정의된 표준 플래그예요. --network none을 주면 코드가 외부로 데이터를 보내려고 시도해도 애초에 연결 자체가 만들어지지 않고, --read-only를 걸어두면 코드 안에 파일 삭제나 덮어쓰기 명령이 섞여 있어도 ‘읽기 전용 파일 시스템입니다’ 같은 오류만 남기고 실패합니다. 컨테이너 밖에서 별도의 오케스트레이션 없이도 파이썬 스크립트 레벨에서 제한을 걸고 싶다면 아래처럼 타임아웃과 최소 환경변수만 넘기는 방식도 함께 씁니다.
import subprocess
result = subprocess.run(
["python3", "generated_code.py"],
timeout=5,
cwd="/sandbox/workspace",
env={"PATH": "/usr/bin"},
capture_output=True,
text=True,
)
print(result.returncode)
print(result.stdout)

timeout=5는 코드가 무한 루프에 빠지거나 응답 없이 멈춰도 5초 뒤 강제 종료되게 하고, env를 최소한으로 넘기면 API 키 같은 민감한 환경변수가 실행되는 코드에 그대로 노출되는 사고를 막아줘요.
샌드박스도 완벽하지 않다는 점
컨테이너 격리를 걸어두면 안심해도 될 것 같지만, 완전한 방어막은 아니에요. 2019년 공개된 runc 취약점(CVE-2019-5736)처럼 컨테이너 런타임 자체의 결함으로 컨테이너 밖 호스트까지 침투가 가능했던 사례가 실제로 있었습니다. 커널을 컨테이너끼리 공유하는 구조인 이상, 런타임에 새로운 취약점이 나오면 격리가 뚫릴 가능성은 항상 남아 있어요. 이런 위험을 더 줄이려면 리눅스 커널을 그대로 쓰지 않고 유저스페이스에서 시스템콜을 가로채는 gVisor 같은 런타임을 붙이는 방법도 있습니다.
또 하나 놓치기 쉬운 지점은, 패키지를 설치하거나 외부 API를 호출해야 하는 코드는 네트워크를 완전히 차단하면 애초에 실행 자체가 안 된다는 점이에요. 이럴 땐 허용할 도메인만 화이트리스트로 열어두는 방식으로 타협해야 하는데, 그 허용 목록을 넓게 잡을수록 격리 효과는 그만큼 줄어듭니다. 그리고 샌드박스는 ‘실행 중 사고’를 막아줄 뿐, 코드가 프로젝트에 병합되기 전 사람이 읽고 검토하는 과정을 대신해주지는 않아요. 격리된 환경에서 문제없이 돌았다고 해서 그 코드 로직이 맞다는 뜻은 아니라는 걸 기억해야 합니다.
지금 코드베이스에 바로 적용할 수 있는 체크리스트
LLM이 생성한 코드를 실행하면서도 안전을 지키고 싶다면, 아래 항목부터 순서대로 점검해보세요.
- 로컬 터미널에 바로 붙여넣지 말고, 위 Docker 명령처럼 네트워크·파일시스템·자원이 제한된 컨테이너 안에서 먼저 실행한다
pip install,npm install같은 패키지 설치 명령도 코드와 분리해서, 설치 전에 패키지 이름이 실제로 존재하고 신뢰할 수 있는지 확인한다- 에이전트가 반복적으로 코드를 실행해야 하는 파이프라인이라면 E2B나 gVisor처럼 이미 검증된 샌드박스 런타임 도입을 검토한다
- 샌드박스 실행이 통과했더라도, 병합 전에는 사람이 diff를 직접 읽는 코드 리뷰 단계를 생략하지 않는다
오늘 바로 할 수 있는 다음 단계는 간단해요. 지금 쓰고 있는 AI 코딩 도구가 생성한 스크립트를 로컬 셸이 아니라 위에 적은 docker run 명령 한 줄로 먼저 돌려보는 것부터 시작해보시길 바랍니다.
