본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 30. 17:00

프롬프트 인젝션(Prompt Injection)은 패치할 수 없습니다. 에이전트가 실행되기 전 치명적인 삼중 위협(Lethal

요약

프롬프트 인젝션은 모델 자체의 패치로 해결할 수 없으며, 에이전트의 역량 조합을 제어해야 합니다. 신뢰할 수 없는 입력, 개인 데이터 접근, 외부 전송이 결합된 '치명적 삼중 위협'을 차단하기 위한 데이터 흐름 기반의 검증 도구 trifecta_gate.py를 소개합니다.

핵심 포인트

  • 프롬프트 인젝션은 모델 내부 패치가 아닌 실행 전 차단이 핵심임
  • 치명적 삼중 위협: 신뢰할 수 없는 입력, 개인 데이터 읽기, 외부 전송의 결합
  • trifecta_gate.py는 그래프 도달 가능성을 통해 위험 경로를 탐지함
  • 단순 플래그 체크가 아닌 데이터 흐름 그래프 기반의 보안 검증 필요

치명적인 삼중 위협(Lethal Trifecta)은 하나의 에이전트가 신뢰할 수 없는 입력(untrusted input)을 읽고, 개인 데이터(private data)를 읽으며, 이를 공유 컨텍스트(shared context)를 통해 외부로 전송할 수 있을 때 완성됩니다. 이는 모델 내에서 패치할 수 있는 문제가 아니므로, 에이전트가 실행되기 전에 이를 차단(gate)해야 합니다. trifecta_gate.py는 매니페스트(manifest)의 도달 가능성(reachability)을 확인합니다. 취약한 피스처(fixture)는 2개의 경로를 반환하며 종료 코드 1(exit 1)을 내보내고, 동일한 역량을 가진 안전한 피스처는 0을 반환하며 종료 코드 0(exit 0)을 내보냅니다.

AI 공개 사항: 저는 AI 어시스턴트와 함께 trifecta_gate.py를 작성했으며, 게시하기 전에 오프라인에서 직접 실행했습니다. 아래 출력 블록의 모든 숫자는 이 포스트에 포함된 합성 매니페스트(synthetic manifests)를 대상으로 Python 3.13.5 표준 라이브러리(stdlib)만 사용하여 로컬에서 실제로 실행한 결과에서 복사한 것입니다. 저는 종료 코드(0 / 1 / 2)를 확인했고, STDOUT을 두 번 해싱하여 바이트 단위로 결정론적(deterministic)임을 확인했으며, 모든 줄을 직접 편집했습니다. 외부 수치(Tenet Security의 Agentjacking 수치, Simon Willison의 용어)는 저의 것이 아니라 원작자의 것이며, 원문 출처를 링크했습니다. 어떤 숫자가 그들의 것이고 어떤 것이 저의 것인지 명시했습니다.

요약하자면:

  • 프롬프트 인젝션(Prompt Injection)은 모델에서 패치할 수 있는 버그가 아닙니다. 모델은 신뢰할 수 있는 지침과 신뢰할 수 없는 데이터를 안정적으로 구분할 수 없으므로, 해결책은 "인젝션을 탐지하는 것"이 아닙니다. 해결책은 위험한 역량 조합이 애초에 도달 가능한 상태가 되지 않도록 차단하는 것입니다.

  • 치명적인 삼중 위협(Lethal Trifecta, Simon Willison이 만든 용어)은 다음과 같은 조합을 의미합니다: 하나의 세션 내에서 에이전트가 신뢰할 수 없는 입력을 읽고, 개인 데이터를 읽으며, 데이터를 외부로 전송할 수 있는 상태입니다. 이 세 가지가 서로 연결되면, 주입된 텍스트(injected text)가 사용자의 비밀 정보를 읽고 이를 외부로 발송할 수 있습니다.

  • trifecta_gate.py는 정적 도구 매니페스트(static tool manifest)를 읽고 그래프 도달 가능성(graph reachability)을 통해 한 가지 질문을 던집니다: "신뢰할 수 없는 입력이 개인 읽기(private read)에 도달하고, 그 읽기가 외부 유출 싱크(egress sink)에 도달하는 경로가 존재하는가?"

  • 핵심 결과: 안전한 매니페스트와 취약한 매니페스트는 **동일한 세 가지 역량(capabilities)**을 선언합니다. 취약한 매니페스트는 2개의 경로를 반환하고 종료 코드 1을 반환합니다. 안전한 매니페스트는 공유 컨텍스트로부터 외부 유출(egress)을 격리하며, 0개의 경로를 반환하고 종료 코드 0을 반환합니다. 게이트(gate)는 플래그 체크리스트가 아니라 데이터 흐름 그래프(data-flow graph)를 기반으로 결정합니다.

  • 표준 라이브러리(Stdlib)만 사용 (json, sys, collections.deque). 네트워크, 모델, 서브프로세스(subprocess) 없음. 실행은 바이트 단위로 결정론적(deterministic)임. 도구와 세 가지 매니페스트(manifests) 모두 이 포스트에 포함되어 있음.

이를 구체화하는 사건

2026년 6월, Tenet Security는 그들이 '에이전트재킹(Agentjacking)'이라 부르는 공격을 공개했습니다. 그 메커니즘은 모욕적일 정도로 단순합니다. 공격자는 공개된 DSN을 사용하여 Sentry 프로젝트에 가짜 에러 이벤트를 밀어 넣습니다. Sentry MCP 서버는 해당 에러를 수정해야 할 실제 버그로 AI 코딩 에이전트에게 전달하며, 이때 메시지 본문에 가짜 ## Resolution 섹션으로 악성 명령을 숨겨둡니다. 에이전트는 이를 읽고 믿으며, 개발자의 머신에서 공격자의 텍스트를 실행합니다.

Tenet의 수치에 따르면: 수동 정찰(passive recon) 결과 주입 가능한 유효한 DSN을 가진 2,388개의 조직이 발견되었으며, 그중 71개는 Tranco 상위 100만 개 목록에 포함되어 있었습니다. 100개 이상의 동의한 조직을 대상으로 한 통제된 테스트에서, Claude Code, Cursor, Codex 등을 대상으로 100개 이상의 에이전트가 주입된 에러에 따라 동작했으며, **85%의 공격 성공률(exploitation success rate)**을 보였습니다. 그들의 테스트에서 탈취된 것들: AWS 비밀 키(secret keys), GitHub OAuth 토큰, SSH 에이전트 소켓, Kubernetes 토큰, ~/.aws/config, ~/.npmrc. 이것은 제 수치가 아니라 Tenet의 보고서(Tenet Security, Agentjacking)에 기재된 Tenet의 수치입니다.

제가 계속해서 다시 읽게 되는 부분은 Sentry의 대응입니다. 그들은 이를 인정하면서도 근본적인 해결을 거부하며, 이를 **

Simon Willison은 2025년 6월 16일에 치명적인 삼중 위협 (Lethal Trifecta)을 다음과 같이 명명했습니다: 개인 데이터에 대한 접근 (access to private data), 신뢰할 수 없는 콘텐츠에 대한 노출 (exposure to untrusted content), 그리고 외부와 통신할 수 있는 능력 (ability to communicate externally). 프롬프트 인젝션 (Prompt Injection)이 왜 모델 수준의 수정에 저항하는지에 대한 그의 논점은 여기서 기초가 됩니다. 그의 표현을 빌리자면, "LLM은 지시 사항이 어디에서 왔는지에 따라 그 중요성을 신뢰할 수 있게 구분할 수 없으며 (LLMs are unable to reliably distinguish the importance of instructions based on where they came from)", "우리는 여전히 이것이 발생하는 것을 100% 신뢰할 수 있게 방지하는 방법을 모릅니다 (we still don't know how to 100% reliably prevent this from happening)." 그의 결론은 모든 공격을 잡아내는 가드레일 (guardrail)을 신뢰하기보다는, 이 세 가지 요소의 조합 자체를 피하라는 것입니다. 그러한 입장들은 그의 것이며, 저는 그 위에 논리를 쌓아 올리고 있습니다.

저는 불편한 진실을 솔직하게 말씀드리겠습니다. 인젝션 시도의 95%를 잡아내는 가드레일은 매우 훌륭하게 들리겠지만, 공격자가 계속해서 재시도한다는 사실을 기억하는 순간 이야기가 달라집니다. 보안에서 95%는 합격률이 아니라 실패율입니다. 만약 신뢰할 수 없는 텍스트와 귀하의 AWS 키 사이를 가로막는 유일한 것이 20번 중 한 번은 틀리는 분류기 (classifier)라면, 귀하는 통제 수단 (control)을 가진 것이 아닙니다. 귀하는 공격자가 자신에게 유리한 결과가 나올 때까지 던질 수 있는 동전을 가진 것뿐입니다.

따라서 레버(lever, 지렛대)를 옮겨야 합니다. "인젝션을 더 잘 탐지하자"가 아닙니다. 그것은 추적 (tracking)이며, 추적은 제가 이 시리즈에서 계속해서 반대해 온 것입니다. 레버는 에이전트 (agent)가 실행되기 전에 **기능 구성 (capability composition)**을 차단 (gate)하는 것입니다. 그래야만 인젝션에 성공하더라도 훔친 정보를 보낼 곳이 없게 됩니다.

논쟁할 수 있을 만큼 날카로운 주장

여기 반증 가능한 버전이 있습니다: 위험은 세 가지 기능의 존재 자체가 아니라, 하나의 공유된 컨텍스트 (shared context)를 통해 이 기능들이 도달 가능한 상태(reachability)에 있다는 것이며, 귀하는 에이전트가 시작되기 전 매니페스트 (manifest)를 통해 이를 정적으로 결정할 수 있습니다.

만약 그 주장이 틀렸다면, 다음 두 가지가 사실이어야 합니다. 첫째, 한 축을 격리하는 것(공유 버스에서 egress를 제거하는 것)이 위험을 줄이지 못해야 합니다. 둘째, 도구 간에 데이터가 어떻게 흐르는지와 관계없이 "세 가지 기능이 모두 존재함"이 항상 "취약함"을 의미해야 합니다. 아래의 안전한 설정(safe fixture)은 이 두 가지 모두에 대한 단일 반례입니다: 세 가지 기능이 존재하지만, 도달 가능한 경로(reachable paths)는 제로이며, 그에 대한 구체적인 이유가 존재합니다.

내재화할 가치가 있는 메커니즘은 **공유 컨텍스트 버스 (shared context bus)**입니다. 기본 에이전트에서는 모든 도구의 출력이 동일한 LLM 컨텍스트 (context)에 담기며, 그 컨텍스트가 다음 도구 호출을 안내합니다. 따라서 한 도구에 의해 읽힌 신뢰할 수 없는 텍스트가 이후의 어떤 도구에도 영향을 미칠 수 있습니다. 이것이 세 가지 기능이 세 개의 독립적인 체크박스가 아닌 이유입니다. 그것들은 공유하는 컨텍스트에 의해 서로 연결된 그래프 상의 노드 (nodes)입니다. Willison의 자체적인 완화 방법은, 예를 들어 오염된 컨텍스트를 절대 볼 수 없는 별도의 샌드박스 처리된 서브 에이전트 (sandboxed sub-agent)에서 egress를 실행함으로써 공유 컨텍스트에서 한 축을 제거하는 것입니다. 게이트 (gate)는 귀하가 실제로 그렇게 했는지를 기계적으로 확인하는 방법일 뿐입니다.

치명적인 삼중 위협 게이트가 무엇을, 어떻게 확인하는가

입력값은 하나의 JSON 파일인 에이전트 매니페스트 (agent manifest)입니다. 각 도구는 정확히 세 가지 클래스(ingests_untrusted, reads_private, can_egress)에서 추출된 capabilities 목록, 선택 사항인 isolated 플래그를 포함하며, 매니페스트는 data_flow 모드를 선언합니다.

shared_context 모드(현실적인 기본값)에서는, 격리되지 않은 모든 도구가 가상 <shared-context> 노드에 데이터를 공급하는 동시에 그 노드에 의해 안내를 받습니다. isolated 도구는 해당 버스에서 제거되며, 이는 구조적으로 고정된 입력을 받고 공유 컨텍스트를 절대 보지 않는 샌드박스 처리된 서브 에이전트를 모델링합니다. explicit 모드에서는 귀하가 선언한 데이터 흐름 에지 (data-flow edges)만이 오염 (taint)을 전달하며, 이는 실제로 도구들을 포인트 투 포인트 (point to point)로 연결했을 때의 정직한 모드입니다.

그다음, 시스템은 유향 그래프 (directed graph)를 구축하고 너비 우선 탐색 (breadth-first search)을 실행합니다. 이때 보고되는 경로가 결정론적 (deterministic)이 되도록 정렬된 순서로 이웃 노드들을 방문합니다. 만약 신뢰할 수 없는 도구 u, 개인용 도구 p, 그리고 외부 유출 (egress) 도구 e가 존재하여, u로부터 p에 도달할 수 있고 p로부터 e에 도달할 수 있다면 '삼중 위협 (trifecta)'이 성립됩니다. 이들은 동일한 도구일 수도 있습니다. 세 가지 능력을 모두 갖춘 하나의 도구 자체가 그 자체로 삼중 위협이 됩니다. 전체 코드는 다음과 같습니다.

#!/usr/bin/env python3
"""trifecta_gate.py - "치명적인 삼중 위협"을 방지하기 위한 실행 전 (PRE-RUN) 정적 게이트.

...

취약한 매니페스트 (manifest): 일반적인 인박스 에이전트

첫 번째 매니페스트는 사람들이 주말 동안 뚝딱 만들어 배포할 법한 LangGraph 스타일의 인박스 어시스턴트입니다. 네 개의 도구가 있습니다. read_email은 공격자가 제어할 수 있는 메시지 본문을 가져오므로, 신뢰할 수 없는 입력 (untrusted input)을 섭취 (ingest)합니다. search_inboxread_contacts는 개인 데이터에 접근합니다. send_email은 누구에게나 메일을 보낼 수 있으므로 외부 유출 (egress)이 가능합니다. 아무것도 격리되어 있지 않으며, 모든 것이 하나의 컨텍스트 (context)를 공유합니다. 실행해 보겠습니다:

$ python3 trifecta_gate.py fixtures/vulnerable_manifest.json
trifecta-gate: agent=langgraph-inbox-assistant mode=shared_context tools=4
capabilities: ingests_untrusted=1 reads_private=2 can_egress=1
...

Exit 1. 두 개의 경로가 발견되었으며, 두 경로 모두 공유 컨텍스트 노드를 통해 실행됩니다. "2개의 경로"가 무엇을 의미하는지 정확히 말하자면, 이 매니페스트 내에 두 개의 별개인 삼중 위협 폐쇄 (trifecta closures)가 존재한다는 뜻입니다. 하나는 read_contacts를 통한 경로이고, 다른 하나는 search_inbox를 통한 경로입니다. 이는 실제 환경에서의 공격 횟수를 의미하는 것도 아니고, 이 파일 이외의 다른 것을 측정하는 것도 아닙니다. 게이트는 구조가 취약함을 알려주고 정확히 어디가 문제인지 지목하고 있습니다. 이메일 본문에 지시 사항을 심어둔 공격자는 원칙적으로 에이전트가 사용자의 연락처를 읽고 이를 전달하도록 만들 수 있습니다. 게이트가 제안하는 방어책은 구조적인 것입니다. 즉, 이러한 형태로는 에이전트를 시작하지 마십시오.

안전한 매니페스트: 동일한 세 가지 기능, 하나는 버스에서 내려짐

이제 핵심을 보여주는 매니페스트(manifest)입니다. 이 매니페스트는 **동일한 네 가지 도구와 동일한 세 가지 기능 클래스(capability classes)**를 선언합니다. 유일한 차이점은 send_emailisolated로 표시되어 있다는 것입니다. 이는 구조적으로 고정된 수신자와 템플릿화된 본문만을 전달받는, 공유 컨텍스트(shared context)를 절대 사용하지 않는 샌드박스화된 서브 에이전트(sandboxed sub-agent)로서 실행되는 이그레스(egress, 외부 유출) 단계를 모델링합니다.

$ python3 trifecta_gate.py fixtures/safe_manifest.json
trifecta-gate: agent=langgraph-inbox-assistant-isolated-send mode=shared_context tools=4
capabilities: ingests_untrusted=1 reads_private=2 can_egress=1
...

Exit 0. 경로가 0개입니다. capabilities 라인을 보십시오. 취약한 실행 결과와 동일한 ingests_untrusted=1 reads_private=2 can_egress=1입니다. 만약 이 게이트(gate)가 체크리스트였다면, 두 매니페스트 모두 3점 만점에 3점을 받았을 것이고 둘 다 실패했을 것입니다. 하지만 그렇지 않습니다. 취약한 것은 실패하고, 안전한 것은 통과합니다. 유일한 차이점은 이그레스(egress)가 공유 버스(shared bus) 위에 있느냐 아니냐 하는 것입니다. 이것이 플래그(flag)를 세는 대신 도달 가능성(reachability)을 통해 이를 수행해야 하는 전체 논거입니다. 체크리스트는 이 둘을 구분할 수 없지만, 그래프(graph)는 구분할 수 있습니다.

이것이 바로 크립토(crypto) 에이전트가 동일한 그림의 더 비용이 많이 드는 버전인 이유이기도 합니다. 라벨을 바꿔보겠습니다: read_pool_data는 온체인(on-chain) 또는 웹의 신뢰할 수 없는 입력을 섭취(ingests)하고, read_wallet은 개인 키나 잔액을 읽으며, sign_and_send_tx는 이그레스(egress)를 수행합니다. 다만 여기서 이그레스는 지갑에서 돈이 나가는 것을 의미합니다. 그래프도 같고, 종료 경로도 같습니다. 다만 성공적인 인젝션(injection)이 연락처 대신 자금을 이동시킨다는 점만 다릅니다. 이 포스트에서 크립토 매니페스트를 전달하지는 않았지만, 구조는 동일하며 해결책 또한 동일합니다: 서명(signing) 단계를 공유 컨텍스트에서 분리하십시오.

종료 코드(exit code)가 곧 게이트입니다

0, 1, 또는 2를 반환하는 목적은 CI(지속적 통합)가 산문(prose)을 읽지 않고도 이를 읽을 수 있게 하기 위함입니다. 에이전트를 실행하거나 매니페스트 변경 사항을 병합(merge)하기 전에 이를 연결하십시오:

if python3 trifecta_gate.py agent_manifest.json; then
    echo "trifecta not reachable - safe to start the agent"
else
...

Exit 0은 실행을 진행하도록 허용합니다. Exit 1은 실행을 중단하고 차단 경로를 출력합니다. Exit 2는 매니페스트 (manifest)가 잘못된 형식임을 의미하며, 잘못된 형식의 매니페스트는 절대로 "안전"한 것으로 읽혀서는 안 됩니다. tools가 리스트 (list) 대신 문자열 (string)인 손상된 픽스처 (fixture)를 입력하면, trifecta-gate: ERROR: manifest.tools must be a non-empty list라는 메시지와 함께 Exit 2로 종료됩니다. 인자 없이 실행하면 사용법을 출력하고 Exit 2로 종료됩니다. "이상 없음"과 "I c

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0