실습 노트
ASCII smuggling에 대응하는 입력 보안: 유니코드 정규화만으로 부족한 이유
메일·문서·AI 에이전트가 읽는 문자열이 화면과 다를 때를 대비해, 유니코드 정규화·보이지 않는 문자 탐지·사람 검수 경계를 구현하는 방법을 정리합니다.
메일·문서·웹페이지를 AI에 넣는 공공기관·기업 담당자라면 화면에 보이는 문자열만으로 입력 안전성을 판단하기 어렵습니다. ASCII smuggling처럼 눈에 보이지 않는 유니코드 문자가 단어 사이에 끼면 키워드 검사, 토큰화, AI가 읽는 내용이 달라질 수 있습니다. 결론은 NFKC 하나로 끝내지 않고 원문 보존, 비교용 문자열, 이상 신호와 사람 승인을 분리하는 것입니다.
메일과 문서에서 무엇이 다르게 보이나요?
같은 입력에는 최소 세 가지 표현이 생길 수 있습니다.
- 사람이 화면에서 읽는 표시 문자열
- 시스템이 실제로 받은 원문 문자열
- 검색·정책 검사에 사용하는 비교용 문자열
Microsoft Security Research는 보이지 않는 Unicode Tags 블록인 U+E0000부터 U+E007F까지가 AI 프롬프트 공격뿐 아니라 피싱 문구의 필터 회피에도 재사용된 사례를 공개했습니다. 예를 들어 funding이라는 단어의 n 앞에 U+E0020을 넣으면 사람에게는 거의 같은 단어로 보일 수 있지만, 원문에는 연속된 funding이 없습니다. Microsoft의 조사 글은 이를 AI 입력과 전통적인 메일 보안이 만나는 사례로 설명합니다.
AI 에이전트가 메일이나 웹페이지를 읽는 경우에는 문제가 한 단계 더 커집니다. OpenAI는 제3자가 문서·웹페이지·메일 안에 숨긴 지시가 모델의 문맥에 들어가 사용자가 의도하지 않은 행동을 유도할 수 있다고 설명합니다. 입력에 접근하는 에이전트의 권한이 넓을수록, 문자열의 차이를 읽는 단계와 실제 행동을 승인하는 단계를 분리해야 합니다. OpenAI의 프롬프트 인젝션 설명도 민감한 데이터 접근을 줄이고 중요한 행동 전에 확인하도록 권고합니다.
유니코드 정규화만 적용하면 충분한가요?
충분하지 않습니다. Unicode Standard Annex #15는 같은 문자를 표현하는 정규화 형식으로 NFC·NFD·NFKC·NFKD를 정의합니다. NFC와 NFD는 주로 정규적 동등성을 맞추고, NFKC와 NFKD는 호환 가능한 표현까지 접습니다.
NFKC는 전각 문자, 원문자, 로마 숫자처럼 비교할 때 같은 의미로 취급할 수 있는 표현을 맞추는 데 유용합니다. 그러나 Unicode 문서는 NFKC·NFKD가 서식 차이를 지울 수 있으므로 임의의 텍스트에 맹목적으로 적용하지 말라고 경고합니다. 사람이 읽고 보관하는 원문을 NFKC 결과로 바꾸면 의미나 표시 정보가 사라질 수 있습니다.
보이지 않는 태그 문자는 별도의 문제입니다. 이 글을 작성하며 Node.js의 String.prototype.normalize("NFKC")를 직접 확인한 결과, fun과 ding 사이에 U+E0020을 넣은 문자열은 NFKC 뒤에도 그 문자를 유지했습니다. 따라서 “NFKC를 적용했으니 숨은 문자가 제거됐다”고 가정하면 안 됩니다.
입력: fun<U+E0020>ding
문자열 검색: "funding" 일치하지 않음
NFKC 이후: fun<U+E0020>ding
태그 제거 후: funding 일치
정규화는 동등한 표현을 맞추는 단계입니다. 특정 보이지 않는 문자를 제거하거나 별도 신호로 기록하는 단계는 정책에 따라 추가해야 합니다.
비교용 문자열은 어떻게 만들까요?
원문을 덮어쓰지 않고 검색·정책 검사에 사용할 별도 문자열을 만듭니다. 다음 예시는 태그 블록과 일부 대표적인 보이지 않는 구분 문자를 비교용 결과에서 제거하고, 원문에 태그가 있었는지를 함께 반환합니다.
const TAG_BLOCK = /[\u{E0000}-\u{E007F}]/gu;
const INVISIBLE_SEPARATORS = /[\u200B\u200C\u2060\uFEFF]/gu;
function toSecurityView(input) {
const normalized = input.normalize("NFKC");
const tagCount = (normalized.match(TAG_BLOCK) ?? []).length;
const comparisonText = normalized
.replace(TAG_BLOCK, "")
.replace(INVISIBLE_SEPARATORS, "");
return {
original: input,
comparisonText,
hasTagBlock: tagCount > 0,
tagCount,
};
}
const view = toSecurityView("fun\u{E0020}ding");
console.log(view.comparisonText.includes("funding")); // true
console.log(view.hasTagBlock, view.tagCount); // true 1
이 함수의 original은 표시·감사·사람 검토를 위한 원문입니다. comparisonText는 키워드·정규식·중복 검사처럼 같은 기준으로 비교해야 하는 작업에 사용합니다. 원문을 자동으로 삭제하거나 바꿔 저장하지 않아야 나중에 “무엇이 들어왔고 어떤 투영으로 검사했는가”를 설명할 수 있습니다.
다만 모든 보이지 않는 문자를 일괄 삭제하면 안 됩니다. 언어별 조합, 이모지, 문서 서식에 필요한 문자가 있을 수 있기 때문입니다. 실제 정책에서는 업무에 필요한 문자 집합을 정하고, 제거 목록과 보존 목록을 버전으로 관리해야 합니다.
태그 문자를 발견하면 바로 차단해야 하나요?
발견 사실은 강한 이상 신호로 삼되, 코드 포인트 하나만으로 악성 여부를 확정하지 않는 편이 안전합니다. Microsoft는 영국·스코틀랜드·웨일스 국기 이모지가 같은 태그 문자 범위를 사용해 단순한 범위 탐지가 오탐을 만들었다고 설명합니다.
실무에서는 다음 순서가 적합합니다.
- 원문에서 태그 블록과 다른 제어·비가시 문자의 위치와 개수를 기록합니다.
- 알려진 정상적인 전체 문자 시퀀스는 별도로 식별합니다.
- 예외가 아닌 태그 문자가 단어 중간에 들어갔는지 비교용 문자열에서 확인합니다.
- 발신자·도메인·URL·인증 결과·첨부파일 유형처럼 서로 다른 신호를 함께 봅니다.
- 외부 발송, 파일 공유, 결재와 같은 되돌리기 어려운 행동은 사람이 원문과 비교용 결과를 확인한 뒤 승인합니다.
여기서 중요한 것은 “태그 문자가 있으면 무조건 허용”하거나 “태그 문자가 있으면 무조건 차단”하는 것이 아닙니다. 정상 예외를 전체 시퀀스로 확인하고, 나머지는 콘텐츠·보낸 사람·행동 권한을 함께 평가하는 다층 규칙이 필요합니다. Microsoft도 특정 유니코드 신호 하나보다 발신자·URL·도메인 평판과 콘텐츠 분석을 함께 사용하는 방식을 설명하며, 조직별 파이프라인에서 직접 시험해야 한다고 안내합니다.
AI 에이전트에는 어떤 경계를 추가해야 하나요?
문자열을 정리해도 문서 안의 지시가 신뢰할 수 있는 지시로 바뀌지는 않습니다. 에이전트가 읽는 외부 문서는 우선 데이터로 취급하고, 사용자의 작업 지시와 문서 안의 문장을 서로 다른 출처로 표시해야 합니다.
- 메일·웹·첨부파일에서 가져온 텍스트는 신뢰되지 않은 입력으로 표시합니다.
- 비교용 문자열에서 숨은 문자와 링크를 검사한 뒤 모델에 전달할 범위를 정합니다.
- 에이전트에는 현재 작업에 필요한 파일·계정·도구만 제공합니다.
- 메일 발송, 외부 공유, 파일 삭제, 결재 요청은 자동 실행하지 않고 승인 단계로 보냅니다.
- 모델의 답변이 아니라 원문, 사용한 변환 규칙, 도구 호출과 승인 기록을 함께 남깁니다.
이 원칙은 특정 공급자의 기능 하나로 해결되지 않습니다. Google은 생성형 모델의 결과에 후처리와 엄격한 수동 평가가 필요하다고 안내합니다. Anthropic도 웹이 적대적인 환경이며 프롬프트 인젝션 방어가 계속되는 연구 과제라고 설명합니다. xAI 역시 모델 개발부터 배포까지 안전 평가를 진행하고 모델 카드와 평가를 공개한다고 밝힙니다. 공통점은 모델의 안전 설명을 읽는 것과 조직의 실제 입력·권한·승인 경계를 시험하는 것이 별개의 일이라는 점입니다.
코드 에이전트도 같은 기준을 적용해야 합니다. GitHub Copilot의 콘텐츠 제외처럼 관리자가 모델 컨텍스트에 들어갈 파일 경계를 정하는 기능은 입력 범위를 줄이는 데 도움이 되지만, 터미널·외부 도구·로그·생성된 패치의 경계까지 대신 정하지는 않습니다. 관련 판단 순서는 GitHub Copilot 운영의 첫 정책은 기본 모델보다 콘텐츠 제외에서 이어서 확인할 수 있습니다.
도입 전에 무엇을 시험하면 좋을까요?
운영 데이터로 시작하지 말고, 숨은 문자를 넣은 비식별 테스트 문서와 정상적인 이모지·다국어 문서를 함께 준비합니다. 다음 표처럼 각 단계의 결과를 따로 기록하면 “검사기가 문자를 없앴는지”, “모델이 원문을 읽었는지”, “사람이 무엇을 승인했는지”를 구분할 수 있습니다.
| 단계 | 확인할 질문 | 남길 결과 |
|---|---|---|
| 수신 | 화면 문자열과 원문 코드 포인트가 다른가요? | 원문 해시, 문자 위치, 입력 출처 |
| 정규화 | NFC·NFKC 중 어떤 비교 규칙을 적용했나요? | 규칙 버전, 변환 전후 길이 |
| 탐지 | 태그·제로 폭 문자와 정상 예외를 구분했나요? | 이상 신호, 예외 사유, 판정 |
| AI 입력 | 모델에 전달한 텍스트가 원문과 어떻게 다른가요? | 전달본, 제거·마스킹 목록 |
| 행동 | 발송·공유·삭제 전에 누가 확인했나요? | 승인자, 시각, 실행 결과 |
테스트가 통과했다는 사실도 전체 보안을 보장하지 않습니다. 메일 보안 제품, 문서 파서, 토크나이저, LLM API가 서로 다른 시점에 다른 변환을 할 수 있기 때문입니다. 실제 서비스의 버전, 계정 권한, 로그 보관, 외부 연결 조건에 맞춰 반복 검증해야 합니다.
다음 업무에서는 무엇부터 정하면 좋을까요?
공공기관이나 기업에서 AI가 메일·문서·첨부파일을 다루기 시작했다면 먼저 “무엇을 입력할 수 있는가”와 “무엇을 자동 실행할 수 없는가”를 한 장으로 정해 보세요. 공개·내부·민감 자료 구분, 파일 업로드 범위, 결과 검수자와 승인 지점을 적어 두면 도구 비교보다 먼저 업무 경계를 검토할 수 있습니다.
공공기관 생성형 AI 교육 전 보안·개인정보 체크리스트에는 계정, 입력 자료, 파일 업로드, 결과 검수와 교육 뒤 보관을 순서대로 확인하는 항목이 정리되어 있습니다. 실제 기관 규정과 계약 조건이 우선이며, 확인되지 않은 자료는 비식별 교육용 예시로 대체해야 합니다.
참고
- Unicode Standard Annex #15: Unicode Normalization Forms
- Microsoft Security Research: ASCII smuggling crosses over from AI prompt injection to phishing evasion
- OpenAI: Understanding prompt injections
- Anthropic: Prompt injection defenses
- Google AI for Developers: Safety and factuality guidance
- xAI: Safety at SpaceXAI
- GitHub Changelog: Content exclusions generally available in Copilot app and CLI