5 분 소요

🚨 시스템 프롬프트 유출과 LLM 서비스 방어 (전체 3편)

  1. 위협 — 유출된 시스템 프롬프트로 할 수 있는 일
  2. 입력단 방어 — 프롬프트 인젝션 탐지와 필터 설계
  3. 실행단 방어 — 에이전트 권한 통제와 운영 완결지금 글

Summary

1편에서 유출된 시스템 프롬프트가 왜 위협인지, 2편에서 입력단에서 인젝션을 거르는 법을 봤어요. 그리고 2편의 결론은 좀 불편했죠. “필터는 결국 뚫린다.”

이번 마지막 글의 전제는 거기서 출발합니다. 모델이 이미 속았다고 가정 하는 거예요. 그래도 실제 피해가 나지 않게 만드는 게 실행단 방어이고, 보안의 진짜 무게중심이 여기에 있습니다.

💡 이 글에서 다루는 것

  • “모델은 신뢰 경계 밖” 이라는 핵심 전제
  • 실행단 방어 4종 — 서버측 권한 검증 · 최소 권한 · 사람 승인 · 도구 출력 불신
  • 권한을 서버에서 다시 검사하는 파이썬 디스패처 예시
  • 3부작 전체를 묶는 운영 체크리스트



1. 핵심 전제 — 모델은 신뢰 경계 밖에 있다

보안 설계에서 가장 중요한 한 줄을 먼저 박을게요.

🚨 LLM 은 신뢰 경계(trust boundary) 안에 있는 컴포넌트가 아닙니다. 사용자 입력, 외부 문서, 그리고 그걸 읽은 모델의 판단까지 — 전부 “신뢰할 수 없는 쪽” 으로 그어야 해요.

웹 보안에 비유하면 쉬워요. 우리는 브라우저(클라이언트)에서 온 데이터를 절대 그대로 믿지 않죠. “이 사용자는 관리자입니다” 라는 값이 요청에 담겨 와도, 서버가 세션으로 다시 검증합니다. 모델도 똑같이 취급해야 해요. 모델이 “이 사용자는 환불 권한이 있어 보이니 환불 함수를 부를게” 라고 판단해도, 그 판단 자체를 믿으면 안 됩니다.

인젝션이 무서운 이유가 여기 있어요. 공격자가 모델의 “판단” 을 오염시킬 수 있다면, 모델의 판단에 권한을 위임한 모든 곳이 그대로 뚫립니다. 그러니 권한 결정은 모델이 아니라 서버 코드가 내려야 해요.



2. 방어 1 — 서버측 권한 검증 (모델의 결정을 다시 검사)

모델이 도구를 호출하겠다고 결정하는 것과, 그 도구가 실제로 실행되는 것 사이에는 반드시 서버측 검증 계층 이 있어야 합니다. 모델은 “무엇을 하고 싶다” 는 의도를 낼 뿐이고, “그걸 해도 되는가” 는 서버가 판단해요.

환불 도구를 예로 들어볼게요. 모델이 issue_refund 를 호출하자고 해도, 실행 직전에 서버가 사용자 권한·주문 소유권·금액 한도 를 다시 검사합니다.

REFUND_LIMIT = 500_000   # 자동 환불 한도 (원)

def execute_refund(user, order, amount):
    # 모델이 호출하자고 결정했어도, 서버가 정책으로 다시 판단한다
    if order["owner_id"] != user["id"]:
        return {"ok": False, "reason": "주문 소유자 불일치"}
    if amount > order["paid"]:
        return {"ok": False, "reason": "결제액 초과 환불 시도"}
    if amount > REFUND_LIMIT:
        return {"ok": False, "reason": "자동 한도 초과 — 사람 승인 필요"}
    return {"ok": True, "refunded": amount}

같은 함수에 정상 요청과, 인젝션으로 부풀려진 요청을 각각 넣어볼게요.

user  = {"id": "u_1001"}
order = {"owner_id": "u_1001", "paid": 120_000}

normal = execute_refund(user, order, amount=120_000)
abuse  = execute_refund(user, order, amount=9_900_000)   # 인젝션으로 과다 환불 유도

print(normal)
print(abuse)
{'ok': True, 'refunded': 120000}
{'ok': False, 'reason': '결제액 초과 환불 시도'}

설령 공격자가 인젝션으로 모델을 꼬드겨 990만 원 환불을 호출하게 만들어도, 서버가 결제액(12만 원)을 넘는 환불 을 그 자리에서 거절합니다. 모델이 속았는지 아닌지는 중요하지 않아요. 실행 계층이 독립적으로 막으니까요.

✅ 포인트는 “모델을 더 똑똑하게 만들기” 가 아니라 “모델의 판단에서 권한을 떼어내기” 예요. 권한 검증을 모델 바깥, 결정론적인 서버 코드로 빼는 순간 인젝션의 파괴력이 크게 줄어듭니다.



3. 방어 2 — 최소 권한 (애초에 위험한 도구를 안 쥐여주기)

서버 검증이 “실행 직전에 막기” 라면, 최소 권한(least privilege)은 애초에 모델이 쥘 수 있는 권한을 줄여두기 예요. 모델에게 노출하는 도구의 권한을 작게 가져갈수록, 뚫렸을 때 피해도 작아집니다.

  • 읽기 우선 — 모델이 부를 수 있는 도구는 가능한 한 읽기 전용으로. 조회·요약은 모델에게 주되, 쓰기·삭제·송금처럼 되돌리기 어려운 건 따로 뺍니다.
  • 스코프 좁히기 — 도구가 쓰는 토큰·DB 계정의 권한을 그 도구에 꼭 필요한 만큼으로 제한. “모든 주문 조회” 가 아니라 “이 사용자의 주문만 조회” 로.
  • 사용자 단위 격리 — 도구 호출은 항상 “지금 이 사용자” 의 컨텍스트 안에서. 모델이 order_id 를 바꿔 불러도 남의 주문엔 손 못 대게.

⚠️ 흔한 실수: 개발 편의로 도구에 광범위한 관리자 토큰을 물려두는 것. 그 토큰의 권한이 곧 인젝션 공격자가 얻는 권한입니다. 도구가 들고 있는 권한 = 최악의 경우 유출되는 권한이라고 보세요.



4. 방어 3 — 사람 승인 (되돌릴 수 없는 일엔 사람을 끼우기)

돈이 나가거나, 데이터가 지워지거나, 외부로 메일이 발송되는 것처럼 되돌리기 어렵고 영향이 큰 행동 은 모델이 단독으로 끝내게 두지 않습니다. 사람의 확인 한 단계(human-in-the-loop)를 끼워요.

2절의 예시에서 한도(REFUND_LIMIT)를 넘는 환불이 사람 승인 필요 로 떨어진 게 바로 이거예요. 자동으로 처리하는 안전한 범위를 정해두고, 그 밖은 큐에 쌓아 운영자가 확인하게 하는 거죠.

판단 기준은 보통 두 축이에요.

자동 처리 OK 사람 승인 필요
되돌릴 수 있나 읽기·조회·임시 저장 송금·삭제·외부 발송
영향 규모 소액·소량 고액·대량·민감 대상

💡 사람 승인은 “모든 걸 사람이 본다” 가 아니에요. 자동화의 안전 범위를 명시적으로 긋고, 그 밖만 사람에게 넘기는 거예요. 범위를 잘 잡으면 대부분은 자동으로 흐르고, 위험한 소수만 사람을 거칩니다.



5. 방어 4 — 도구 출력도 신뢰하지 않기

마지막으로 자주 빠뜨리는 지점. 도구가 돌려준 결과 도 안전하지 않습니다. 특히 외부를 긁어오는 도구(웹검색·문서조회·메일읽기)의 출력은 그 자체가 간접 인젝션의 통로 예요.

흐름을 보면 이래요. 모델이 웹검색 도구를 부른다 → 그 도구가 공격자가 심어둔 페이지를 긁어온다 → 그 본문 속 숨은 지시가 모델의 다음 행동에 섞여 들어간다. 입력단에서 사용자 메시지만 검사했다면, 이 경로는 그대로 열려 있어요.

그래서 도구 출력도 모델에게 다시 먹이기 전에 한 번 거릅니다.

  • 외부 콘텐츠는 2편의 구분자 분리·휴리스틱·분류기를 똑같이 한 번 더 통과시킨다.
  • 도구 결과는 “지시” 가 아니라 “데이터” 임을 시스템 프롬프트에서 다시 못 박는다.
  • 가능하면 외부 콘텐츠에서 실행 가능한 지시처럼 보이는 부분(숨은 텍스트, 비정상 마크업)을 정제(sanitize)한다.

🚨 입력단 방어를 사용자 메시지에만 걸고 도구 출력은 그냥 통과시키는 게 가장 흔한 구멍이에요. 모델에 들어가는 모든 텍스트 에 같은 불신을 적용하세요.



6. 운영 체크리스트 — 3부작 전체 묶기

세 편에 걸쳐 쌓은 방어를, 실제로 서비스에 적용할 때 훑을 체크리스트로 정리할게요.

설계 원칙 (1편)

  • 시스템 프롬프트는 “새어나갈 수 있는 설명서” 로 설계 — 비밀을 그 안에 박지 않기
  • security by obscurity 금지 — 규칙이 공개돼도 무너지지 않는 구조

입력단 (2편)

  • 지시와 데이터를 구분자로 분리, 데이터 영역은 명령 아님을 명시
  • 휴리스틱 필터로 흔한 인젝션 문구 1차 차단
  • 분류기(모더레이션)로 우회 표현 2차 탐지
  • 허니토큰으로 시스템 프롬프트 유출 순간 포착

실행단 (3편)

  • 권한 결정은 모델이 아니라 서버 코드가 — 실행 직전 재검증
  • 최소 권한 — 읽기 우선, 토큰 스코프 좁히기, 사용자 단위 격리
  • 되돌릴 수 없는 행동엔 사람 승인 단계
  • 도구 출력도 신뢰 안 함 — 외부 콘텐츠에 입력단 방어 재적용

운영

  • 모든 도구 호출 감사 로그 — 누가, 어떤 입력으로, 무엇을 불렀는지
  • 비정상 패턴(과다 호출, 반복 실패, 허니토큰 검출) 알림
  • 공개된 유출물·탈옥 패턴으로 정기 레드팀

✅ 이 체크리스트의 무게중심은 아래쪽(실행단·운영)에 있어요. 입력단은 공격을 줄이고, 실행단은 피해를 막습니다. 둘 다 필요하지만, 하나만 고르라면 실행단 이에요.



마치며 — 시리즈를 닫으며

세 편을 한 줄씩으로 요약하면 이렇습니다.

  1. 위협 — 유출된 시스템 프롬프트는 공격자에게 약점 지도이자 내부 설계도가 된다.
  2. 입력단 방어 — 인젝션을 입력에서 거르되, 필터는 결국 뚫린다는 가정 위에 설계한다.
  3. 실행단 방어 — 모델이 속아도 피해가 안 나게, 권한과 부작용을 서버에서 통제한다.

관통하는 한 문장은 처음부터 끝까지 같았어요. 모델을 신뢰 경계 밖에 두기. 프롬프트가 새어나가도, 인젝션이 통해도, 모델이 잘못된 판단을 내려도 — 진짜 피해를 막는 둑이 그 아래 서버 로직에 있으면 서비스는 버팁니다.

유출 뉴스가 떴을 때 막연히 불안해할 필요는 없어요. 위 체크리스트를 한 번 훑어보고, 비밀이 프롬프트 밖에 있는지·권한이 서버에서 통제되는지만 확인하면 됩니다. 그게 되어 있으면 유출은 사고가 아니라 점검의 계기로 끝나요.

긴 시리즈 읽어주셔서 고맙습니다. 다음엔 또 다른 주제로 찾아올게요. 🎉


← 이전 글: (2/3) 입력단 방어 — 프롬프트 인젝션 탐지와 필터 설계