🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Sandboxing & Execution Control 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2026. 2. 3. · 4 Views

Sandboxing & Execution Control 완벽 가이드

AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.


목차

  1. 샌드박스_환경_구성
  2. 컨테이너_기반_격리
  3. 리소스_제한_설정
  4. 타임아웃_및_킬_스위치
  5. 안전한_코드_실행
  6. 악성_동작_탐지

1. 샌드박스 환경 구성

김개발 씨는 AI 에이전트 프로젝트를 진행하던 중 고민에 빠졌습니다. 사용자가 입력한 코드를 서버에서 실행해야 하는데, 만약 악성 코드가 들어오면 어떻게 될까요?

선배 박시니어 씨가 다가와 말했습니다. "샌드박스 환경을 구성해야지, 그냥 실행하면 큰일 나."

샌드박스는 한마디로 외부와 격리된 안전한 놀이터입니다. 마치 어린이 놀이터의 모래사장처럼, 아이들이 무엇을 하든 그 안에서만 영향을 미치고 바깥 세상에는 피해를 주지 않습니다.

이것을 제대로 이해하면 신뢰할 수 없는 코드도 안전하게 실행할 수 있습니다.

다음 코드를 살펴봅시다.

import tempfile
import os
from pathlib import Path

class SandboxEnvironment:
    def __init__(self):
        # 격리된 임시 디렉토리 생성
        self.sandbox_dir = tempfile.mkdtemp(prefix="sandbox_")
        self.allowed_paths = [self.sandbox_dir]

    def is_path_allowed(self, path: str) -> bool:
        # 샌드박스 영역 내 경로인지 검증
        resolved = Path(path).resolve()
        return any(str(resolved).startswith(p) for p in self.allowed_paths)

    def cleanup(self):
        # 샌드박스 환경 정리
        import shutil
        shutil.rmtree(self.sandbox_dir, ignore_errors=True)

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 회사에서 AI 코딩 도우미 서비스를 개발하고 있는데, 사용자가 작성한 코드를 서버에서 실행해서 결과를 보여줘야 합니다.

처음에는 단순하게 생각했습니다. "그냥 exec() 함수로 실행하면 되지 않나?" 선배 박시니어 씨가 깜짝 놀라며 말했습니다.

"잠깐, 그러면 누군가 os.system('rm -rf /')를 보내면 어떻게 되겠어? 서버가 통째로 날아가!" 그렇다면 샌드박스란 정확히 무엇일까요?

쉽게 비유하자면, 샌드박스는 마치 과학 실험실의 안전 캐비닛과 같습니다. 위험한 화학물질을 다룰 때 연구원은 밀폐된 캐비닛 안에서 작업합니다.

만약 폭발이 일어나더라도 캐비닛 내부에서만 피해가 발생하고, 실험실 전체가 위험해지지는 않습니다. 이처럼 샌드박스도 코드의 실행 범위를 제한된 공간으로 한정합니다.

샌드박스가 없던 시절에는 어땠을까요? 개발자들은 사용자 입력을 무조건 신뢰하거나, 아예 코드 실행 기능을 제공하지 않았습니다.

신뢰하면 보안 사고가 터지고, 기능을 빼면 서비스 경쟁력이 떨어졌습니다. 더 큰 문제는 한 번 보안이 뚫리면 전체 시스템이 위험에 노출된다는 것이었습니다.

바로 이런 문제를 해결하기 위해 샌드박스 개념이 등장했습니다. 샌드박스를 사용하면 파일 시스템 접근을 제한할 수 있습니다.

또한 네트워크 통신을 차단하거나 특정 주소만 허용할 수 있습니다. 무엇보다 시스템 명령어 실행을 원천 봉쇄할 수 있다는 큰 장점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 tempfile.mkdtemp() 함수로 임시 디렉토리를 생성합니다.

이 디렉토리가 바로 샌드박스의 경계가 됩니다. 다음으로 is_path_allowed() 메서드에서는 요청된 경로가 샌드박스 내부인지 검증합니다.

마지막으로 cleanup() 메서드는 작업이 끝나면 흔적을 깨끗이 지웁니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 온라인 코딩 교육 플랫폼을 운영한다고 가정해봅시다. 학생들이 제출한 코드를 채점하려면 서버에서 실행해야 합니다.

이때 샌드박스를 활용하면 학생의 코드가 채점 서버를 해킹하는 것을 방지할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 샌드박스 경로 검증을 문자열 비교로만 하는 것입니다. 교묘한 경로 조작(../../etc/passwd)으로 샌드박스를 탈출할 수 있습니다.

따라서 반드시 Path().resolve()로 절대 경로를 구한 뒤 검증해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 임시 디렉토리를 만들어서 거기서만 작업하게 하는 거군요!" 샌드박스 환경 구성을 제대로 이해하면 신뢰할 수 없는 코드도 안심하고 실행할 수 있습니다.

여러분도 오늘 배운 내용을 AI 에이전트 프로젝트에 적용해 보세요.

실전 팁

💡 - 샌드박스 디렉토리는 반드시 임시 영역에 생성하고, 사용 후 즉시 삭제하세요

  • 경로 검증 시 심볼릭 링크 우회 공격도 고려해야 합니다

2. 컨테이너 기반 격리

김개발 씨가 샌드박스를 구현했지만 뭔가 찜찜합니다. 파일 시스템만 격리해서는 부족한 것 같았습니다.

박시니어 씨가 말했습니다. "파이썬 레벨 샌드박스는 한계가 있어.

진짜 격리가 필요하면 컨테이너를 써야 해."

컨테이너는 운영체제 수준에서 프로세스를 완전히 격리하는 기술입니다. 마치 호텔의 객실처럼, 각 투숙객은 자신만의 공간에서 생활하며 다른 객실에 영향을 주지 않습니다.

Docker 같은 컨테이너 기술을 활용하면 CPU, 메모리, 네트워크까지 완벽하게 분리할 수 있습니다.

다음 코드를 살펴봅시다.

import docker
import uuid

class ContainerSandbox:
    def __init__(self):
        self.client = docker.from_env()
        self.container_id = None

    def execute_code(self, code: str, timeout: int = 30) -> dict:
        # 고유한 컨테이너 이름 생성
        container_name = f"sandbox_{uuid.uuid4().hex[:8]}"

        # 격리된 컨테이너에서 코드 실행
        container = self.client.containers.run(
            image="python:3.11-slim",
            command=["python", "-c", code],
            name=container_name,
            network_disabled=True,  # 네트워크 차단
            mem_limit="128m",       # 메모리 제한
            cpu_period=100000,
            cpu_quota=50000,        # CPU 50% 제한
            detach=True,
            remove=False
        )

        # 결과 대기 및 수집
        result = container.wait(timeout=timeout)
        logs = container.logs().decode("utf-8")
        container.remove(force=True)

        return {"exit_code": result["StatusCode"], "output": logs}

김개발 씨는 파이썬 코드만으로 샌드박스를 구현했지만, 테스트 중 불안한 점을 발견했습니다. 파이썬의 ctypes 모듈이나 일부 C 확장 라이브러리를 사용하면 샌드박스를 우회할 수 있다는 것이었습니다.

박시니어 씨가 설명했습니다. "애플리케이션 레벨 샌드박스는 결국 같은 프로세스 안에서 돌아가잖아.

진짜 보안이 필요하면 운영체제 레벨에서 격리해야 해." 그렇다면 컨테이너 기반 격리란 정확히 무엇일까요? 쉽게 비유하자면, 컨테이너는 마치 완전히 독립된 원룸과 같습니다.

일반 샌드박스가 한 집 안에서 방문만 잠근 것이라면, 컨테이너는 아예 다른 건물에서 생활하는 것입니다. 화재가 나도, 소음을 내도 옆 건물에는 전혀 영향이 없습니다.

컨테이너가 없던 시절에는 어땠을까요? 개발자들은 가상 머신(VM)을 사용했습니다.

하지만 VM은 무거웠습니다. 하나의 코드를 실행하기 위해 전체 운영체제를 부팅해야 했으니까요.

수백 명의 사용자가 동시에 코드를 제출하면 서버가 감당할 수 없었습니다. 바로 이런 문제를 해결하기 위해 컨테이너 기술이 등장했습니다.

컨테이너를 사용하면 수 초 만에 격리 환경을 생성할 수 있습니다. 또한 호스트 커널을 공유하므로 VM보다 훨씬 가볍습니다.

무엇보다 리소스 제한을 세밀하게 설정할 수 있다는 장점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 docker.from_env()로 Docker 데몬에 연결합니다. containers.run() 메서드에서 주목할 점은 network_disabled=True로 네트워크를 완전히 차단한다는 것입니다.

mem_limit와 cpu_quota 옵션으로 리소스도 제한합니다. 마지막으로 실행이 끝나면 container.remove()로 흔적을 깨끗이 지웁니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 LeetCode나 프로그래머스 같은 코딩 테스트 플랫폼을 생각해봅시다.

수천 명이 동시에 코드를 제출하고, 각각의 코드가 독립적으로 채점되어야 합니다. 컨테이너를 활용하면 각 제출을 완벽하게 격리하면서도 빠르게 처리할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 컨테이너 이미지를 신뢰하는 것입니다.

공식 이미지라도 취약점이 있을 수 있습니다. 따라서 최소한의 기능만 포함된 slim 이미지를 사용하고, 정기적으로 업데이트해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. Docker를 적용한 후 보안 테스트를 다시 해보니, 이전에 우회되던 공격들이 모두 차단되었습니다.

"운영체제 레벨 격리가 이렇게 강력하군요!" 컨테이너 기반 격리를 제대로 이해하면 어떤 악성 코드가 들어와도 시스템을 안전하게 보호할 수 있습니다.

실전 팁

💡 - 컨테이너 이미지는 반드시 신뢰할 수 있는 공식 이미지를 사용하세요

  • 불필요한 권한(privileged 모드)은 절대 부여하지 마세요

3. 리소스 제한 설정

김개발 씨의 서비스가 런칭되었습니다. 그런데 어느 날 서버가 멈춰버렸습니다.

로그를 확인해보니 누군가 while True: pass 같은 무한 루프 코드를 실행한 것이었습니다. "격리만 하면 끝이 아니었네..." 김개발 씨는 한숨을 쉬었습니다.

리소스 제한은 실행되는 코드가 사용할 수 있는 CPU, 메모리, 디스크 공간을 제한하는 것입니다. 마치 휴대폰 데이터 요금제처럼, 월 10GB를 넘으면 속도가 느려지거나 차단되는 것과 같습니다.

이를 통해 악의적이거나 비효율적인 코드가 전체 시스템을 마비시키는 것을 방지합니다.

다음 코드를 살펴봅시다.

import resource
import signal
import os

class ResourceLimiter:
    def __init__(self, max_memory_mb: int = 128, max_cpu_seconds: int = 5):
        self.max_memory = max_memory_mb * 1024 * 1024
        self.max_cpu = max_cpu_seconds

    def apply_limits(self):
        # 메모리 제한 설정 (가상 메모리)
        resource.setrlimit(resource.RLIMIT_AS,
                          (self.max_memory, self.max_memory))

        # CPU 시간 제한 설정
        resource.setrlimit(resource.RLIMIT_CPU,
                          (self.max_cpu, self.max_cpu))

        # 생성 가능한 프로세스 수 제한
        resource.setrlimit(resource.RLIMIT_NPROC, (10, 10))

        # 파일 디스크립터 수 제한
        resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64))

    def run_with_limits(self, func):
        pid = os.fork()
        if pid == 0:  # 자식 프로세스
            self.apply_limits()
            try:
                result = func()
                os._exit(0)
            except MemoryError:
                os._exit(137)  # OOM 시그널
        else:  # 부모 프로세스
            _, status = os.waitpid(pid, 0)
            return os.WEXITSTATUS(status)

김개발 씨는 컨테이너로 격리까지 완벽하게 했다고 생각했습니다. 그런데 서비스 런칭 일주일 만에 서버가 다운되었습니다.

원인을 분석해보니 한 사용자가 재귀 함수를 잘못 작성해서 메모리를 8GB나 잡아먹은 것이었습니다. 박시니어 씨가 말했습니다.

"격리와 제한은 다른 개념이야. 격리는 '어디서' 실행하느냐고, 제한은 '얼마나' 쓸 수 있느냐야." 그렇다면 리소스 제한이란 정확히 무엇일까요?

쉽게 비유하자면, 리소스 제한은 마치 뷔페 식당의 시간제한과 같습니다. 돈을 냈으니 음식은 마음껏 먹을 수 있지만, 2시간이 지나면 나가야 합니다.

아무리 배가 고파도 식당을 독점할 수는 없습니다. 리소스 제한도 각 프로세스가 공정하게 시스템 자원을 나눠 쓰도록 강제합니다.

리소스 제한이 없던 시절에는 어땠을까요? 하나의 프로세스가 CPU를 100% 점유하면 다른 모든 프로세스가 멈췄습니다.

메모리 누수가 발생하면 서버 전체가 OOM(Out of Memory) 킬러에 의해 강제 종료되었습니다. 더 큰 문제는 의도치 않은 버그와 악의적인 공격을 구분할 수 없었다는 것입니다.

바로 이런 문제를 해결하기 위해 운영체제 수준의 리소스 제한 기능이 발전했습니다. 리소스 제한을 사용하면 CPU 사용 시간을 초 단위로 제한할 수 있습니다.

또한 메모리 할당량을 바이트 단위로 통제할 수 있습니다. 무엇보다 포크 폭탄 같은 공격도 프로세스 수 제한으로 방어할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 resource.setrlimit() 함수로 다양한 제한을 설정합니다.

RLIMIT_AS는 가상 메모리를 제한하고, RLIMIT_CPU는 CPU 시간을 제한합니다. RLIMIT_NPROC는 생성 가능한 프로세스 수를 제한하여 포크 폭탄 공격을 방어합니다.

run_with_limits() 메서드는 fork()로 자식 프로세스를 만들어 제한을 적용한 후 함수를 실행합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 클라우드 IDE 서비스를 운영한다고 가정해봅시다. 무료 사용자는 메모리 256MB, CPU 1코어로 제한하고, 유료 사용자는 더 많은 자원을 할당할 수 있습니다.

이런 차등화된 서비스가 리소스 제한으로 가능해집니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 제한을 너무 빡빡하게 설정하는 것입니다. 정상적인 코드도 실행되지 못하면 사용자 경험이 나빠집니다.

따라서 여유 있는 기본값을 설정하되, 모니터링을 통해 점진적으로 조정해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

리소스 제한을 적용한 후 같은 공격이 들어왔지만, 이번에는 해당 프로세스만 종료되고 서버는 멀쩡했습니다. "제한과 격리, 둘 다 필요하군요!" 리소스 제한을 제대로 이해하면 안정적이고 공정한 멀티테넌트 서비스를 구축할 수 있습니다.

실전 팁

💡 - 메모리 제한은 RLIMIT_AS(가상 메모리)와 RLIMIT_DATA(힙) 중 상황에 맞게 선택하세요

  • 제한에 걸렸을 때 사용자에게 명확한 에러 메시지를 제공하세요

4. 타임아웃 및 킬 스위치

김개발 씨는 리소스 제한까지 완벽하게 설정했습니다. 그런데 가끔 코드가 5초 제한을 초과해도 계속 실행되는 경우가 있었습니다.

알고 보니 I/O 대기 시간은 CPU 시간에 포함되지 않는 것이었습니다. "네트워크 요청을 무한정 기다리면 어떡하지?"

타임아웃은 작업이 지정된 시간 내에 완료되지 않으면 강제로 중단하는 메커니즘입니다. 킬 스위치는 더 나아가 이상 징후가 감지되면 즉시 프로세스를 종료하는 비상 장치입니다.

마치 전기 차단기가 과전류를 감지하면 자동으로 전원을 끊는 것처럼, 시스템을 보호하는 최후의 방어선입니다.

다음 코드를 살펴봅시다.

import signal
import subprocess
import threading
from contextlib import contextmanager

class ExecutionController:
    def __init__(self, timeout_seconds: int = 30):
        self.timeout = timeout_seconds
        self.process = None

    @contextmanager
    def timeout_context(self):
        def timeout_handler(signum, frame):
            raise TimeoutError(f"Execution exceeded {self.timeout} seconds")

        # 시그널 핸들러 등록
        old_handler = signal.signal(signal.SIGALRM, timeout_handler)
        signal.alarm(self.timeout)

        try:
            yield
        finally:
            signal.alarm(0)
            signal.signal(signal.SIGALRM, old_handler)

    def execute_with_kill_switch(self, command: list) -> dict:
        self.process = subprocess.Popen(
            command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )

        try:
            stdout, stderr = self.process.communicate(timeout=self.timeout)
            return {"success": True, "output": stdout.decode()}
        except subprocess.TimeoutExpired:
            # 킬 스위치 발동: 강제 종료
            self.process.kill()
            self.process.wait()
            return {"success": False, "error": "Process killed due to timeout"}

김개발 씨는 CPU 시간 제한만으로 충분하다고 생각했습니다. 그런데 어느 날 테스터가 이상한 리포트를 보냈습니다.

"sleep(1000000)을 실행했는데 서버가 안 죽어요." CPU를 사용하지 않으니 CPU 시간 제한에 걸리지 않는 것이었습니다. 박시니어 씨가 설명했습니다.

"CPU 시간과 실제 시간(wall-clock time)은 달라. I/O 대기나 sleep은 CPU 시간에 안 잡혀." 그렇다면 타임아웃과 킬 스위치란 정확히 무엇일까요?

쉽게 비유하자면, 타임아웃은 마치 시험의 제한 시간과 같습니다. 아무리 열심히 풀고 있어도 시간이 끝나면 답안지를 제출해야 합니다.

킬 스위치는 더 강력합니다. 부정행위가 발견되면 시험 도중에라도 즉시 퇴장시키는 감독관과 같습니다.

타임아웃이 없던 시절에는 어땠을까요? 무한 루프에 빠진 프로세스를 일일이 찾아서 수동으로 kill해야 했습니다.

관리자가 자리를 비운 사이에 서버가 좀비 프로세스로 가득 차기도 했습니다. 더 큰 문제는 정상 종료와 비정상 종료를 구분하기 어려웠다는 것입니다.

바로 이런 문제를 해결하기 위해 체계적인 타임아웃 관리가 필요해졌습니다. 타임아웃을 사용하면 실제 경과 시간 기준으로 제한을 걸 수 있습니다.

또한 subprocess.communicate(timeout=)으로 외부 프로세스도 제어할 수 있습니다. 무엇보다 signal.alarm으로 현재 프로세스에도 타임아웃을 적용할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 timeout_context()는 컨텍스트 매니저로, with 문과 함께 사용합니다.

signal.SIGALRM을 활용해 지정된 시간이 지나면 TimeoutError를 발생시킵니다. execute_with_kill_switch() 메서드는 subprocess.Popen으로 외부 프로세스를 실행하고, 타임아웃 시 process.kill()로 강제 종료합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 에이전트가 외부 API를 호출하는 상황을 생각해봅시다.

상대 서버가 응답하지 않으면 우리 서비스도 멈춰버립니다. 타임아웃을 설정해두면 일정 시간 후 포기하고 대체 로직을 실행할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 타임아웃을 너무 짧게 설정하는 것입니다.

정상적인 작업도 중간에 끊기면 데이터 정합성 문제가 발생할 수 있습니다. 따라서 작업의 특성에 맞는 적절한 타임아웃 값을 설정해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. wall-clock 타임아웃을 추가한 후 sleep 공격도 완벽하게 차단되었습니다.

"CPU 시간과 실제 시간, 둘 다 체크해야 하는구나!" 타임아웃과 킬 스위치를 제대로 이해하면 어떤 상황에서도 시스템이 응답 불능 상태에 빠지지 않도록 보호할 수 있습니다.

실전 팁

💡 - CPU 시간 제한과 wall-clock 타임아웃은 별개이니 둘 다 설정하세요

  • 킬 스위치 발동 시 리소스 정리(cleanup) 로직도 반드시 포함하세요

5. 안전한 코드 실행

김개발 씨는 격리, 제한, 타임아웃까지 모두 적용했습니다. 하지만 마지막으로 한 가지 고민이 남았습니다.

"코드를 실행하기 전에 미리 위험한 패턴을 걸러낼 수는 없을까?" 사전 예방이 사후 대응보다 낫다는 것을 알기 때문입니다.

안전한 코드 실행은 코드를 실행하기 전에 정적 분석으로 위험 요소를 탐지하고, 화이트리스트 기반으로 허용된 기능만 사용하도록 제한하는 것입니다. 마치 공항 보안 검색대처럼, 탑승 전에 위험물을 미리 걸러내는 것과 같습니다.

실행 중 방어와 함께 사용하면 다층 보안을 구축할 수 있습니다.

다음 코드를 살펴봅시다.

import ast
import builtins
from typing import Set

class SafeCodeExecutor:
    # 허용된 내장 함수 화이트리스트
    ALLOWED_BUILTINS = {
        'print', 'len', 'range', 'str', 'int', 'float', 'bool',
        'list', 'dict', 'set', 'tuple', 'sum', 'min', 'max',
        'sorted', 'enumerate', 'zip', 'map', 'filter'
    }

    # 금지된 모듈 블랙리스트
    FORBIDDEN_MODULES = {'os', 'sys', 'subprocess', 'socket', 'shutil'}

    def validate_code(self, code: str) -> tuple[bool, str]:
        try:
            tree = ast.parse(code)
        except SyntaxError as e:
            return False, f"Syntax error: {e}"

        for node in ast.walk(tree):
            # import 문 검사
            if isinstance(node, (ast.Import, ast.ImportFrom)):
                module = node.names[0].name if isinstance(node, ast.Import) \
                         else node.module
                if module.split('.')[0] in self.FORBIDDEN_MODULES:
                    return False, f"Forbidden module: {module}"

            # 위험한 함수 호출 검사
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    if node.func.id in ('eval', 'exec', 'compile', 'open'):
                        return False, f"Forbidden function: {node.func.id}"

        return True, "Code is safe"

    def execute_safely(self, code: str) -> dict:
        is_safe, message = self.validate_code(code)
        if not is_safe:
            return {"success": False, "error": message}

        # 제한된 내장 함수만 포함한 실행 환경
        safe_builtins = {k: getattr(builtins, k) for k in self.ALLOWED_BUILTINS}
        safe_globals = {"__builtins__": safe_builtins}

        try:
            exec(code, safe_globals)
            return {"success": True, "output": "Executed successfully"}
        except Exception as e:
            return {"success": False, "error": str(e)}

김개발 씨는 모든 보안 장치를 갖췄지만, 여전히 불안했습니다. 악성 코드가 실행되기 전에 미리 걸러낼 수 없을까요?

소 잃고 외양간 고치는 것보다 처음부터 도둑이 들어오지 못하게 막는 것이 낫지 않을까요? 박시니어 씨가 조언했습니다.

"정적 분석이라고 들어봤어? 코드를 실행하지 않고 분석해서 위험 요소를 찾아내는 기술이야." 그렇다면 안전한 코드 실행이란 정확히 무엇일까요?

쉽게 비유하자면, 안전한 코드 실행은 마치 공항의 X-ray 검색대와 같습니다. 승객의 가방을 열어보지 않고도 내부에 위험물이 있는지 알 수 있습니다.

마찬가지로 코드를 실행하지 않고도 AST(Abstract Syntax Tree)를 분석해서 위험한 패턴을 탐지할 수 있습니다. 사전 검증이 없던 시절에는 어땠을까요?

모든 코드를 일단 실행해보고 문제가 생기면 대응했습니다. 악성 코드가 격리 환경 안에서 실행되더라도, 리소스를 낭비하고 로그를 오염시켰습니다.

더 큰 문제는 제로데이 취약점으로 격리를 우회하면 손쓸 방법이 없었다는 것입니다. 바로 이런 문제를 해결하기 위해 사전 검증과 화이트리스트 기반 실행이 발전했습니다.

안전한 코드 실행을 사용하면 import os 같은 위험한 모듈 임포트를 사전에 차단할 수 있습니다. 또한 eval, exec 같은 동적 실행 함수도 금지할 수 있습니다.

무엇보다 허용된 내장 함수만 사용하도록 실행 환경을 제한할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 ast.parse()로 코드를 구문 분석 트리로 변환합니다. ast.walk()로 트리의 모든 노드를 순회하면서 import 문과 함수 호출을 검사합니다.

FORBIDDEN_MODULES에 있는 모듈이나 eval, exec 같은 함수를 발견하면 거부합니다. execute_safely()에서는 safe_builtins로 제한된 환경을 구성해서 실행합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 Jupyter Notebook 기반 데이터 분석 플랫폼을 생각해봅시다.

사용자들이 pandas, numpy는 자유롭게 사용하되, 파일 시스템이나 네트워크 접근은 막고 싶습니다. 화이트리스트 기반 실행 환경을 구축하면 이런 세밀한 제어가 가능합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 정적 분석만 믿는 것입니다.

교묘한 난독화나 동적 코드 생성으로 정적 분석을 우회할 수 있습니다. 따라서 정적 분석은 첫 번째 방어선일 뿐, 런타임 격리와 함께 사용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 정적 분석을 추가한 후 대부분의 악성 코드가 실행 전에 차단되었습니다.

"다층 방어가 답이군요. 하나만 믿으면 안 돼!" 안전한 코드 실행을 제대로 이해하면 사전 예방과 사후 대응을 모두 갖춘 견고한 보안 체계를 구축할 수 있습니다.

실전 팁

💡 - AST 분석은 첫 번째 필터이지, 완벽한 방어가 아닙니다

  • 화이트리스트는 블랙리스트보다 안전하지만, 너무 제한적이면 사용성이 떨어집니다

6. 악성 동작 탐지

김개발 씨는 이제 자신감이 생겼습니다. 그런데 보안팀에서 연락이 왔습니다.

"정상적인 것처럼 보이지만 실제로는 악성인 코드가 있어요. 실행 중에 이상 동작을 탐지하는 시스템이 필요합니다."

악성 동작 탐지는 코드가 실행되는 동안 행위를 모니터링하여 이상 패턴을 발견하는 기술입니다. 마치 CCTV가 도둑의 행동을 실시간으로 감시하는 것처럼, 시스템 콜, 파일 접근, 네트워크 활동 등을 추적하여 악의적인 행위를 탐지합니다.

정적 분석을 우회한 공격도 런타임에서 잡아낼 수 있습니다.

다음 코드를 살펴봅시다.

import sys
import logging
from collections import defaultdict
from functools import wraps

class BehaviorMonitor:
    def __init__(self, threshold: int = 100):
        self.call_counts = defaultdict(int)
        self.threshold = threshold
        self.suspicious_patterns = []

        # 로깅 설정
        logging.basicConfig(level=logging.WARNING)
        self.logger = logging.getLogger("BehaviorMonitor")

    def monitor_function(self, func_name: str):
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                self.call_counts[func_name] += 1

                # 과도한 호출 탐지
                if self.call_counts[func_name] > self.threshold:
                    self.suspicious_patterns.append({
                        "type": "excessive_calls",
                        "function": func_name,
                        "count": self.call_counts[func_name]
                    })
                    self.logger.warning(
                        f"Suspicious: {func_name} called {self.call_counts[func_name]} times"
                    )
                    raise SecurityException("Excessive function calls detected")

                return func(*args, **kwargs)
            return wrapper
        return decorator

    def analyze_behavior(self) -> dict:
        risk_score = len(self.suspicious_patterns) * 10
        risk_score += sum(c for c in self.call_counts.values() if c > 50)

        return {
            "total_calls": dict(self.call_counts),
            "suspicious_patterns": self.suspicious_patterns,
            "risk_score": min(risk_score, 100)
        }

class SecurityException(Exception):
    pass

김개발 씨는 모든 보안 장치를 갖추고 안심하고 있었습니다. 그런데 보안팀에서 흥미로운 샘플을 보내왔습니다.

정적 분석을 통과한 코드였지만, 실행해보니 이상한 행동을 했습니다. 정상적인 문자열 처리처럼 보이지만, 실제로는 메모리를 점점 많이 사용하고 있었습니다.

박시니어 씨가 설명했습니다. "이건 슬로우 DoS 공격이야.

한 번에 터지지 않고 서서히 시스템을 잠식하지. 행위 기반 탐지가 필요해." 그렇다면 악성 동작 탐지란 정확히 무엇일까요?

쉽게 비유하자면, 악성 동작 탐지는 마치 은행의 이상 거래 탐지 시스템과 같습니다. 카드가 해외에서 갑자기 사용되거나, 평소와 다른 패턴의 거래가 발생하면 알림이 옵니다.

마찬가지로 코드의 실행 패턴을 분석해서 평소와 다른 행동을 탐지합니다. 행위 기반 탐지가 없던 시절에는 어땠을까요?

정적 분석만으로는 모든 공격을 잡을 수 없었습니다. 난독화된 코드, 동적으로 생성되는 페이로드, 시간차 공격 등 다양한 우회 기법이 존재했습니다.

더 큰 문제는 새로운 공격 기법이 나오면 패턴을 업데이트해야 했다는 것입니다. 바로 이런 문제를 해결하기 위해 행위 기반 탐지 시스템이 발전했습니다.

악성 동작 탐지를 사용하면 과도한 함수 호출 패턴을 실시간으로 감지할 수 있습니다. 또한 리소스 사용량의 이상 증가를 탐지할 수 있습니다.

무엇보다 알려지지 않은 새로운 공격도 행위 분석으로 발견할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 BehaviorMonitor 클래스는 함수 호출 횟수를 추적합니다. monitor_function 데코레이터는 특정 함수를 감싸서 호출될 때마다 카운트를 증가시킵니다.

threshold를 초과하면 suspicious_patterns에 기록하고 SecurityException을 발생시킵니다. analyze_behavior() 메서드는 전체적인 위험 점수를 계산합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 AI 챗봇 서비스를 운영한다고 가정해봅시다.

사용자가 플러그인을 설치해서 기능을 확장할 수 있습니다. 악성 플러그인이 설치되더라도 행위 모니터링으로 이상 동작을 탐지하면 피해를 최소화할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 임계값을 너무 낮게 설정하는 것입니다.

정상적인 코드도 오탐(false positive)으로 차단되면 사용자 불만이 커집니다. 따라서 충분한 데이터를 모아서 적절한 임계값을 찾아야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 행위 기반 탐지를 추가한 후 슬로우 DoS 공격도 중간에 차단되었습니다.

"정적 분석과 동적 분석, 둘 다 필요하구나!" 악성 동작 탐지를 제대로 이해하면 알려진 공격뿐 아니라 새로운 유형의 공격도 방어할 수 있는 능동적인 보안 체계를 구축할 수 있습니다.

실전 팁

💡 - 임계값은 정상 사용 패턴을 충분히 분석한 후 설정하세요

  • 오탐을 줄이기 위해 여러 지표를 종합적으로 판단하는 것이 좋습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#Sandbox#Container#Security#ExecutionControl#AI,Agent,Security

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스