가짜 편지 — 오염된 메모리가 송금을 돌린다
Patlan 외(2025)는 ElizaOS 에이전트의 메모리에 Discord 로 올린 위조 '지난 대화'를 심었고, 나중에 X 에서 들어온 송금 요청이 공격자 주소로 갔습니다(Sepolia, 연구 데모). 아래는 그 흐름을 주인 @kindofblue91 로 다시 짠 시뮬레이션입니다. 모델 호출도, 서명도, 트랜잭션도 없습니다.
①부터 누르면 공격이, ①을 건너뛰고 ②부터 누르면 정상 송금이 진행됩니다. 방어를 켜고 다시 해 보세요.
무엇을 보여 주나
모든 플랫폼이 하나의 메모리 로그를 공유하면, 누구든 글 한 줄로 '주인이 예전에 한 말'을 만들 수 있습니다. 간접 프롬프트 인젝션(Greshake 외)이 한 번으로 끝나지 않고 저장되어 계속 작동합니다.
에이전트는 자기 키로 서명하므로 서명은 진짜이고, 금액도 한도 안입니다. 검증자가 그 둘만 보면 5/5 로 통과합니다 — Bybit 의 '유효한 서명 세 개'와 같은 구조입니다.
방어를 켜면 각 검증자가 주인이 서명한 X 요청에서 수신자를 직접 꺼내 tx.to 와 비교합니다. 메모리는 이 검사의 입력이 아니므로, 메모리를 아무리 오염시켜도 결과가 바뀌지 않습니다.
실제 계정으로 하면: Discord 로 명령, X 로 영수증
시나리오: Discord 에서 "Alice 에게 10 jUSD 보내 줘", 영수증은 X 에. 계정마다 맡은 일이 하나씩 있습니다. Discord 는 지휘 채널입니다. 거기서 명령을 내리고, 시스템은 그 명령이 정말 내게서 왔다는 걸 증명할 수 있습니다. X 는 공개 게시판입니다. 에이전트는 거기에 올리기만 하고, 절대 명령을 받지 않습니다.
| # | 어디서 어디로 | 무슨 일이 |
|---|---|---|
| ① | Discord → 서버 | 내가 /pay alice 10 을 입력합니다. Discord 가 서명한 요청을 서버가 DISCORD_PUBLIC_KEY 로 검증합니다. |
| ② | 서버 | 보낸 사람의 사용자 id 가 jay 인지 확인합니다(허용 목록). |
| ③ | 서버 → Discord | 3초 안에 "처리 중…" 으로 먼저 답합니다(지연 응답). |
| ④ | 서버 | Cloud Tasks 작업에서 에이전트가 송금을 계획합니다. 받는 사람 "alice" 는 내 명령에서 가져오고, 메모리나 대화 기록에서는 절대 가져오지 않습니다. |
| ⑤ | 서버 → Discord | [확인] [취소] 버튼이 달린 후속 메시지를 보냅니다. |
| ⑥ | Discord → 서버 | 확인을 누르면 서명을 다시 검증한 뒤에야 10 jUSD 를 보냅니다(테스트넷). |
| ⑦ | 서버 → Discord | 인터랙션 토큰으로 "완료, tx 0x…" 를 알립니다. |
| ⑧ | 서버 → X | @kindofblue91 로 영수증을 게시합니다: "alice 에게 10 jUSD · tx 0x…" |
각 키가 하는 일
| 키 | 쓰이는 곳 | 비밀? |
|---|---|---|
DISCORD_PUBLIC_KEY | ①, ⑥: 요청이 정말 Discord 에서 왔는지 증명 | 아니오 |
DISCORD_APPLICATION_ID | /pay 등록, ⑤·⑦ 후속 메시지 | 아니오 |
DISCORD_BOT_TOKEN | 명령 등록, 또는 인터랙션 없이 채널에 글쓰기 | 예 |
X OAuth 2.0 user token (tweet.write) | ⑧, @kindofblue91 로 게시 | 예 |
안전하게 만드는 규칙 세 가지
다른 Discord 메시지나 X 멘션은 전부 데이터일 뿐입니다. 에이전트가 읽을 수는 있어도, 그것으로 송금이 일어날 수는 없습니다. 위 시뮬레이션의 ① 오염 메시지가 바로 이 "다른 메시지"입니다.
메모리에서 가져오지 않습니다. 서명은 내가 아니라 Discord 가 하고(①), 보낸 사람이 나인지는 id 로 따로 확인합니다(②). 위 시뮬레이션에서 "방어"를 켰을 때 검증자들이 하는 일이 이것입니다.
⑥ 전에는 아무것도 움직이지 않습니다. 에이전트가 X 멘션을 명령으로 읽으면, 전 세계 누구나 트윗으로 명령을 내릴 수 있게 됩니다.
더 단순한 변형
- Discord 만: ①–⑦ 단계. 플랜의 T4 는 이 중 읽기 전용 부분(
/ask,/status)만 만듭니다./pay는 계획에 없습니다. - X 만: 크론 작업으로 매일 "에이전트 상태" 트윗. 들어오는 경로가 없으니 주입할 틈도 없습니다.
이 페이지는 Discord 나 X 에 연결되지 않습니다. 위의 흐름은 설명이고, 실제 봇은 아직 없습니다. T4 는 읽기 전용 봇을 계획합니다. X 게시는 X API 요금을 먼저 확인해야 합니다.
기술 노트
각 항목이 실제로 무엇을 보여주고 어떻게 동작하는지 — 위 카드보다 자세한 기술 설명입니다.
목적: 에이전트 보안 논의는 대개 모델의 판단을 다룹니다. Patlan 외(2025)는 더 싼 길을 보였습니다. ElizaOS 에이전트는 모든 플랫폼에 메모리 로그 하나를 쓰기 때문에, Discord 의 공격자가 올린 글이 나중에 '주인이 한 말'로 다시 읽히고, 주인이 X 로 요청한 송금이 공격자에게 갑니다(Sepolia, 연구 데모). 이 데모는 두 가지를 눈에 보이게 합니다. 메모리는 신뢰할 수 없는 입력이고, 저장되는 순간 간접 프롬프트 인젝션이 지속성을 얻습니다. 그리고 검증자들이 모두 같은 성질, 즉 에이전트 자신의 서명만 본다면 아무것도 더하지 못합니다. 오염된 에이전트도 그 서명은 정직하게 만들어 내니까요.
동작 방식: 다섯 단계짜리 클라이언트 상태 기계이고 LLM 은 호출하지 않습니다. ①은 출처 표시 없이 위조 줄 하나를 공유 메모리에 씁니다. ②는 주인의 X 요청 "Alice 에게 10 jUSD 보내 줘"입니다. ③에서 에이전트가 메모리로 수신자를 정하므로 오염된 경우 0xBAD 로 계획합니다. ④ 검증자 다섯이 쿼럼 3으로 투표합니다. 기본 모드에서는 각자 에이전트 서명과 금액 한도를 보는데 둘 다 참이라 5/5 입니다. 방어 모드에서는 각자 주인이 서명한 요청에서 수신자를 다시 꺼내 tx.to 와 비교합니다. 메모리는 입력이 아니므로 오염된 계획은 0/5 입니다. ⑤는 쿼럼일 때만 돈을 옮깁니다. 가리키는 실제 해법은 의도 바인딩입니다. 무엇을 지불해도 되는지는 에이전트의 문맥이 아니라 사람이 서명한 요청이 정합니다.