본문 바로가기
C.W.K.
Stream
Lesson 06 of 06 · published

도구에는 API와 같은 권한 심사가 필요해

~14 min · security, permissions, side-effects

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

도구 설명도 외부로 전송돼

description은 해당 도구가 포함된 요청마다 Anthropic으로 나가. 내부 호스트명과 경로, 테이블 이름, 비밀을 짐작하게 하는 문구를 적으면 프롬프트 데이터가 돼 버려. 설명은 공개 계약처럼 검토하고 배포 전에 민감한 정보를 걷어 내.

쓰기는 사람의 확인을 받아

검색·조회·읽기 도구는 대개 자동 실행해도 피해가 제한적이야. 생성·수정·삭제 도구는 실행 전에 명시적인 승인을 받을 가치가 있어. Agent SDK는 권한 프롬프트를 제공하고 Client SDK 기반 루프에서는 직접 만들어야 해. 어떤 경우에도 모델의 한 문장만으로 파괴적 부수 효과를 확정하지 마.

코드와 셸은 격리해서 실행해

명령 실행이나 파일 접근 도구에는 제한된 환경변수, 한 루트 안인지 검사한 경로 인자, 아는 목록으로 제한한 플래그, 시간 예산, 출력 크기 상한을 둬. 경로는 인자마다 검사해야 해. 작업 디렉터리만으로는 절대 경로도 ..도 못 막고, grep -R이나 -f<파일> 같은 플래그는 경로 검사를 옆으로 돌아가거든. 명령 이름 허용 목록은 가드지 샌드박스가 아니야. 신뢰할 수 없는 입력이라면 운영체제가 프로세스를 가두는 층도 필요해. Anthropic의 code_execution이 샌드박스에서 도는 것처럼 커스텀 도구도 피해 범위를 구조적으로 줄여야 해.

원칙: 새 도구 하나는 신뢰하지 않은 사용자에게 연 API 하나와 같아. 권한과 입력, 부수 효과를 같은 눈으로 감사해.

Code

Write tool 주변 permission gate·python
import os, json

DESTRUCTIVE = {"delete_file", "drop_table", "send_email"}

async def gated_invoke(block, ask_user):
    args_str = json.dumps(block.input, indent=2)
    if block.name in DESTRUCTIVE:
        approved = await ask_user(f"Allow {block.name} with args:\n{args_str}")
        if not approved:
            return {"type": "tool_result", "tool_use_id": block.id,
                    "is_error": True, "content": "user denied permission"}
    handler = HANDLERS[block.name]
    out = await handler(**block.input)
    return {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(out)}
경로 인자는 한 루트 안인지 검사하고, 아는 플래그만 받고, 시간 예산을 둔 허용 목록 셸 도구·python
import asyncio, os, shlex

ROOT = os.path.realpath("/sandbox")
ALLOWED = {"ls", "cat", "grep", "head", "tail"}
FLAGS = {"-l", "-a", "-la", "-1", "-n", "-i", "-c", "-v", "-w", "-E", "-F", "-r", "-h"}

def inside_root(arg: str) -> bool:
    # 절대 경로, .., 심볼릭 링크 모두 비교 전에 풀어
    path = os.path.realpath(os.path.join(ROOT, arg))
    return path == ROOT or path.startswith(ROOT + os.sep)

async def guarded_shell(command: str, timeout: float = 5.0):
    parts = shlex.split(command)
    if not parts or parts[0] not in ALLOWED:
        raise PermissionError(f"command not in allowlist: {parts[0] if parts else ''}")
    # cwd만으로는 아무것도 못 가둬: `cat /etc/passwd`도 `cat ../x`도 그냥 돌아.
    for arg in parts[1:]:
        if arg.startswith("-"):
            # 아는 단독 플래그만: -R/-S(링크 따라가기), -f<파일>, --opt=경로는 전부 막아
            if arg not in FLAGS:
                raise PermissionError(f"flag not allowed: {arg}")
        elif not inside_root(arg):
            raise PermissionError(f"path escapes the sandbox: {arg}")
    proc = await asyncio.create_subprocess_exec(
        *parts,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        cwd=ROOT,
        env={"PATH": "/usr/bin:/bin"},  # 최소 환경
    )
    try:
        out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout)
    except asyncio.TimeoutError:
        proc.kill()
        raise TimeoutError("command exceeded time budget")
    return {"stdout": out.decode()[:8192], "stderr": err.decode()[:2048], "returncode": proc.returncode}

# 이건 가드지 샌드박스가 아니야. 신뢰할 수 없는 입력이면 운영체제가 프로세스를
# 가두는 곳(컨테이너, VM, macOS sandbox-exec)에서도 돌려.

External links

Exercise

부수 효과가 있는 도구 하나에 권한 문과 샌드박스 경계, 감사 기록을 추가해. 승인 없이는 거부되는 시험으로 세 경계를 증명해.
Hint
확인 없이 DB 테이블을 지울 수 있다면 운영 사고를 기다리고 있는 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.