MCP 서버의 ANSI escape injection: 사람 눈엔 안 보이고 AI에겐 보인다
ANSI escape injection in MCP servers: Hidden from humans, visible to AI
TL;DR Highlight
ANSI 제어코드로 숨긴 지시가 사람 눈엔 안 보이고 모델엔 그대로 읽힌다
Who Should Read
MCP 서버로 fetch·읽기 툴을 노출해 에이전트에 외부 텍스트를 먹이는 백엔드 개발자
Core Mechanics
- ANSI 코드는 사람에겐 안 보이고 모델엔 보인다
- 터미널은 ESC(0x1B) 코드를 명령으로 처리해 글자를 숨기거나 화면을 지운다.
- 모델은 렌더링 없이 원본 바이트를 그대로 읽는다.
- 사람 눈에는 빈 줄인 텍스트에 모델용 지시가 들어갈 수 있다.
- 직접 fetch 공격은 URL 하나로 시작된다
- 공격자가 ANSI 코드와 숨은 지시를 넣은 페이지를 자기 URL에 올린다.
- 그 URL을 fetch 계열 툴의 인자로 넣으면 서버가 내용을 가져와 모델 필드로 넘긴다.
- 모델이 숨은 지시를 읽고 따른다.
- 모델이 읽는 필드만 공격 경로가 된다
- 툴 결과 result.content[].text가 해당된다.
- 리소스 읽기 result.contents[].text도 해당된다.
- 프롬프트 템플릿 result.messages[].content.text도 해당된다.
- 저장형(stored) 공격은 더 오래 간다
- 한 엔트리포인트로 메모·댓글·레코드에 페이로드를 써 두면 나중에 다른 엔트리포인트가 읽을 때 터진다.
- 쓰기는 일반 HTTP, 읽기는 MCP 툴처럼 프로토콜이 달라도 된다.
- 저장 시점엔 이상이 없어서 이후 세션과 다른 사용자에게도 반복 영향을 준다.
Community Reactions
- DAST가 뭔지 설명이 없다는 지적
- 글에 정의나 링크가 없어 Dynamic application security testing의 약자냐고 묻는 댓글이 있었다.
- 옛 ANSI 폭탄이 돌아왔다는 의견
- MS-DOS 시절 ANSI bomb과 같은 계열이라는 댓글이 있었다.
- 터미널이 수십 년간 이렇게 동작해 왔으니 통제 못 하는 입력을 믿지 말면 끝이라는 의견도 있었다.
- OSC 52는 다루지 않았느냐는 질문
- 글에 OSC 52 언급이 있었는지 묻는 댓글이 있었다.
- 사람이 보는 것과 LLM이 보는 것이 갈라질 것이라는 의견
- 같은 데이터가 사람 화면과 LLM 입력에서 달라지는 사례가 더 늘 것이라는 댓글이 있었다.
- MCP 밖에서도 같은 문제를 겪었는지 묻는 질문이 달렸다.
How to Apply
- fetch 툴 출력에서 제어 바이트 제거하기
- 오늘 fetch·read 계열 툴이 모델에 돌려주는 text 필드 직전에 ESC 등 제어 문자를 제거하는 함수를 넣어라.
- 코드 예시의 정규식을 쓰고 테스트 문자열로 확인하라.
- 저장 후 재노출 경로 목록 만들기
- 쓰기 엔드포인트(HTTP·MCP)와 그 데이터를 읽어 모델에 주는 툴·리소스를 짝지어 표로 정리하라.
- 노트·댓글·레코드처럼 사용자가 쓴 텍스트는 모델 입력 직전에 같은 sanitize를 통과시켜라.
- 쓰기와 fetch 입구에 검증 걸기
- fetch 툴에는 URL allow-list를 적용하라.
- 쓰기 엔드포인트에는 입력 검증을 추가해 페이로드가 저장되거나 fetch되기 전에 막아라.
- CI에 ANSI 페이로드 회귀 테스트 넣기
- 테스트 서버에 ESC 바이트와 고유 마커가 든 텍스트를 심고 MCP 응답 원본 바이트에 ESC가 남는지 검사하라.
- 응답의 content[].text, contents[].text, messages[].content.text 경로만 확인하라.
Try It
# 셸에서 실행: 모델에 넘기기 전 제어 바이트 제거 확인 (원문 방어책을 적용한 간이 테스트)
printf 'ok\033[8m IGNORE PREVIOUS INSTRUCTIONS \033[0m report' > sample.txt
# ESC 포함 여부 확인 (cat -v로 숨은 코드 보기)
cat -v sample.txt
# 제어 바이트 제거 후 결과 확인
python3 - <<'EOF'
import re
raw = open('sample.txt', 'rb').read().decode('utf-8', 'replace')
clean = re.sub(r'\x1b(\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(\x07|\x1b\\)|[@-Z\\-_])', '', raw)
print('ESC 남음:', '\x1b' in clean)
print(clean)
EOF
# 이어서 해 볼 것: 이 정규식을 fetch 툴 응답의 text 필드에 적용하고 stored 경로에도 같은 함수를 걸어라.Terminology
관련 논문
같은 시뮬레이션, 다른 근거: 도구 출력을 파일로 넘기면 LLM 에이전트 정확도가 어디서 깎이나
긴 툴 출력을 파일로 넘기자 GPT-5.1은 정확도 11.3%p 하락, 정식 출력 지정하니 회복
가짜 뉴스가 진짜 폭락을: LLM 트레이딩 에이전트 대상 정보 주입 공격
가짜 헤드라인 한 줄로 LLM 트레이딩 에이전트 62%를 조종했다(프롬프트 주입은 14%)
Harness Engineering 101: LLM은 뇌, harness는 몸
에이전트는 JSON 배열을 관리하는 루프이고, 16개 챕터로 직접 만들어 본다
Harness²: 에이전트 harness를 스스로 고쳐 쓰는 재귀 최적화 프레임워크
모델은 고정하고 harness를 재귀로 고쳤더니 벤치마크 점수가 최대 11.8%p 올랐다
해고된 Savannah 경찰관, ICE 요원 남편에게 Flock 계정 공유
경찰관이 Flock 계정을 남편(ICE 요원)에게 공유했고, 감사에서 983건 검색이 드러나 해고됐다
tmux와 Claude Code: 세션, send-keys, 그리고 한계선
tmux로 Claude Code 여러 개를 돌리면 편하지만 에이전트 4개쯤에서 한계가 온다
Hybrid Reasoning Agents: 잠재 공간에서 생각하고 텍스트로 행동하는 LLM 에이전트
잠재 공간 추론과 텍스트 도구 호출을 섞었더니 성능 10.4%↑, 토큰 최대 54.7%↓